radiant-docs 0.1.38 → 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 +8 -5
- package/template/src/components/user/CodeGroup.astro +262 -14
- package/template/src/components/user/ComponentPreviewBlock.astro +4 -3
- package/template/src/components/user/Image.astro +43 -53
- package/template/src/components/user/Tabs.astro +128 -23
- 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 +85 -1
|
@@ -27,17 +27,55 @@ if (labels.length === 0) {
|
|
|
27
27
|
|
|
28
28
|
<div x-data="{
|
|
29
29
|
activeTab: 0,
|
|
30
|
+
previousTab: null,
|
|
31
|
+
transitionDirection: 1,
|
|
32
|
+
isTransitioning: false,
|
|
33
|
+
transitionDurationMs: 300,
|
|
34
|
+
transitionTimeout: null,
|
|
30
35
|
containerHeight: 'auto',
|
|
31
36
|
markerStyle: { left: null, width: null },
|
|
37
|
+
resizeHandler: null,
|
|
32
38
|
init() {
|
|
39
|
+
this.resizeHandler = () => {
|
|
40
|
+
this.updateMarker(this.activeTab);
|
|
41
|
+
this.updateHeight(this.isTransitioning);
|
|
42
|
+
};
|
|
43
|
+
window.addEventListener('resize', this.resizeHandler);
|
|
44
|
+
|
|
33
45
|
this.$nextTick(() => {
|
|
34
46
|
this.updateMarker(this.activeTab);
|
|
35
47
|
this.updateHeight();
|
|
36
48
|
});
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
},
|
|
50
|
+
destroy() {
|
|
51
|
+
if (this.resizeHandler) {
|
|
52
|
+
window.removeEventListener('resize', this.resizeHandler);
|
|
53
|
+
}
|
|
54
|
+
if (this.transitionTimeout) {
|
|
55
|
+
window.clearTimeout(this.transitionTimeout);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
selectTab(index) {
|
|
59
|
+
if (index === this.activeTab) return;
|
|
60
|
+
|
|
61
|
+
if (this.transitionTimeout) {
|
|
62
|
+
window.clearTimeout(this.transitionTimeout);
|
|
63
|
+
this.transitionTimeout = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.previousTab = this.activeTab;
|
|
67
|
+
this.transitionDirection = index > this.activeTab ? 1 : -1;
|
|
68
|
+
this.isTransitioning = true;
|
|
69
|
+
this.activeTab = index;
|
|
70
|
+
this.updateMarker(this.activeTab);
|
|
71
|
+
this.updateHeight(true);
|
|
72
|
+
|
|
73
|
+
this.transitionTimeout = window.setTimeout(() => {
|
|
74
|
+
this.isTransitioning = false;
|
|
75
|
+
this.previousTab = null;
|
|
76
|
+
this.transitionTimeout = null;
|
|
39
77
|
this.updateHeight();
|
|
40
|
-
});
|
|
78
|
+
}, this.transitionDurationMs);
|
|
41
79
|
},
|
|
42
80
|
updateMarker(index) {
|
|
43
81
|
const el = this.$refs['tab-' + index];
|
|
@@ -48,20 +86,54 @@ if (labels.length === 0) {
|
|
|
48
86
|
};
|
|
49
87
|
}
|
|
50
88
|
},
|
|
51
|
-
|
|
89
|
+
getPanelStyle(index) {
|
|
90
|
+
const base = 'position: absolute; inset: 0;';
|
|
91
|
+
|
|
92
|
+
const isActive = index === this.activeTab;
|
|
93
|
+
const isPrevious = this.isTransitioning && index === this.previousTab;
|
|
94
|
+
|
|
95
|
+
if (!isActive && !isPrevious) {
|
|
96
|
+
return `${base} opacity: 0; pointer-events: none; visibility: hidden; z-index: 0;`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!this.isTransitioning) {
|
|
100
|
+
return 'position: relative; transform: translateX(0); opacity: 1; pointer-events: auto; visibility: visible; z-index: 1;';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isActive) {
|
|
104
|
+
const animationName =
|
|
105
|
+
this.transitionDirection === 1
|
|
106
|
+
? 'rd-tabs-slide-in-from-right'
|
|
107
|
+
: 'rd-tabs-slide-in-from-left';
|
|
108
|
+
return `${base} opacity: 1; pointer-events: auto; visibility: visible; z-index: 2; animation: ${animationName} ${this.transitionDurationMs}ms ease-in-out both;`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const animationName =
|
|
112
|
+
this.transitionDirection === 1
|
|
113
|
+
? 'rd-tabs-slide-out-to-left'
|
|
114
|
+
: 'rd-tabs-slide-out-to-right';
|
|
115
|
+
return `${base} opacity: 1; pointer-events: none; visibility: visible; z-index: 1; animation: ${animationName} ${this.transitionDurationMs}ms ease-in-out both;`;
|
|
116
|
+
},
|
|
117
|
+
updateHeight(includePrevious = false) {
|
|
52
118
|
this.$nextTick(() => {
|
|
53
|
-
// We look for the internal wrapper or the content div specifically
|
|
54
119
|
const activeSlide = this.$refs['content-' + this.activeTab];
|
|
55
|
-
if (activeSlide)
|
|
56
|
-
|
|
57
|
-
|
|
120
|
+
if (!activeSlide) return;
|
|
121
|
+
|
|
122
|
+
let nextHeight = activeSlide.scrollHeight;
|
|
123
|
+
if (includePrevious && this.previousTab !== null) {
|
|
124
|
+
const previousSlide = this.$refs['content-' + this.previousTab];
|
|
125
|
+
if (previousSlide) {
|
|
126
|
+
nextHeight = Math.max(nextHeight, previousSlide.scrollHeight);
|
|
127
|
+
}
|
|
58
128
|
}
|
|
129
|
+
|
|
130
|
+
this.containerHeight = nextHeight + 'px';
|
|
59
131
|
});
|
|
60
132
|
}
|
|
61
133
|
}"
|
|
62
134
|
class="my-5">
|
|
63
135
|
<ul
|
|
64
|
-
class="relative isolate not-prose flex w-
|
|
136
|
+
class="relative isolate not-prose flex w-full max-w-full min-w-0 rounded-lg border border-neutral-100 bg-neutral-100/80 p-[3px] dark:border-none dark:bg-neutral-800/50"
|
|
65
137
|
>
|
|
66
138
|
<div
|
|
67
139
|
class="absolute top-[3px] bottom-[3px] -z-10 flex items-center justify-center rounded-md bg-white shadow-sm transition-all duration-300 ease-out dark:bg-neutral-700/30 dark:border dark:border-neutral-700/40 dark:shadow-black/30."
|
|
@@ -74,12 +146,12 @@ class="my-5">
|
|
|
74
146
|
</div>
|
|
75
147
|
|
|
76
148
|
{ labels.map((label, index) => (
|
|
77
|
-
<li>
|
|
149
|
+
<li class="min-w-0 flex-1">
|
|
78
150
|
<button
|
|
79
151
|
type="button"
|
|
80
152
|
x-ref={`tab-${index}`}
|
|
81
|
-
@click={`
|
|
82
|
-
class="relative
|
|
153
|
+
@click={`selectTab(${index})`}
|
|
154
|
+
class="relative flex h-[32px] w-full min-w-0 max-w-full cursor-pointer items-center gap-2 px-3 text-sm font-medium transition-colors duration-200"
|
|
83
155
|
style={index === 0 ? "" : ""}
|
|
84
156
|
class:list={[index === 0 ? "text-foreground" : "text-muted-foreground"]}
|
|
85
157
|
:class={`{
|
|
@@ -88,30 +160,63 @@ class="my-5">
|
|
|
88
160
|
}`}
|
|
89
161
|
>
|
|
90
162
|
{icons[index] && <Icon name={icons[index]} class="size-4 shrink-0" />}
|
|
91
|
-
{label}
|
|
163
|
+
<span class="min-w-0 flex-1 truncate" title={label}>{label}</span>
|
|
92
164
|
</button>
|
|
93
165
|
</li>
|
|
94
166
|
)) }
|
|
95
167
|
</ul>
|
|
96
168
|
|
|
97
169
|
<div
|
|
98
|
-
class="mt-4 overflow-hidden transition-[height] duration-300 ease-in-out"
|
|
170
|
+
class="relative mt-4 overflow-hidden transition-[height] duration-300 ease-in-out"
|
|
99
171
|
:style="'height: ' + containerHeight"
|
|
100
172
|
>
|
|
101
|
-
<div
|
|
102
|
-
class="flex items-start transition-transform duration-300 ease-in-out"
|
|
103
|
-
:style="'transform: translateX(-' + (activeTab * 100) + '%)'"
|
|
104
|
-
>
|
|
105
173
|
{ tabContents.map((content, index) => (
|
|
106
|
-
// We add a ref here so we can measure the height
|
|
107
174
|
<div
|
|
175
|
+
{...(index !== 0 ? { "x-cloak": true } : {})}
|
|
108
176
|
x-ref={`content-${index}`}
|
|
109
|
-
class="w-full
|
|
110
|
-
:style={`
|
|
111
|
-
style={index === 0 ? '
|
|
177
|
+
class="w-full"
|
|
178
|
+
:style={`getPanelStyle(${index})`}
|
|
179
|
+
style={index === 0 ? 'position: relative;' : ''}
|
|
112
180
|
set:html={content}
|
|
113
181
|
/>
|
|
114
182
|
)) }
|
|
115
|
-
</div>
|
|
116
183
|
</div>
|
|
117
184
|
</div>
|
|
185
|
+
|
|
186
|
+
<style>
|
|
187
|
+
@keyframes rd-tabs-slide-in-from-right {
|
|
188
|
+
from {
|
|
189
|
+
transform: translateX(100%);
|
|
190
|
+
}
|
|
191
|
+
to {
|
|
192
|
+
transform: translateX(0);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@keyframes rd-tabs-slide-in-from-left {
|
|
197
|
+
from {
|
|
198
|
+
transform: translateX(-100%);
|
|
199
|
+
}
|
|
200
|
+
to {
|
|
201
|
+
transform: translateX(0);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@keyframes rd-tabs-slide-out-to-left {
|
|
206
|
+
from {
|
|
207
|
+
transform: translateX(0);
|
|
208
|
+
}
|
|
209
|
+
to {
|
|
210
|
+
transform: translateX(-100%);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@keyframes rd-tabs-slide-out-to-right {
|
|
215
|
+
from {
|
|
216
|
+
transform: translateX(0);
|
|
217
|
+
}
|
|
218
|
+
to {
|
|
219
|
+
transform: translateX(100%);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
</style>
|
|
@@ -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>,
|