radiant-docs 0.1.34 → 0.1.38

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 (56) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +27 -0
  3. package/template/package-lock.json +1027 -513
  4. package/template/package.json +3 -2
  5. package/template/scripts/generate-proxy-allowed-origins.mjs +217 -0
  6. package/template/scripts/generate-robots-txt.mjs +19 -0
  7. package/template/scripts/stamp-image-versions.mjs +63 -11
  8. package/template/src/components/Footer.astro +1 -1
  9. package/template/src/components/Header.astro +9 -9
  10. package/template/src/components/LogoLink.astro +2 -1
  11. package/template/src/components/OpenApiPage.astro +18 -18
  12. package/template/src/components/Search.astro +18 -18
  13. package/template/src/components/Sidebar.astro +4 -2
  14. package/template/src/components/SidebarDropdown.astro +82 -79
  15. package/template/src/components/SidebarGroup.astro +3 -0
  16. package/template/src/components/SidebarMenu.astro +14 -1
  17. package/template/src/components/SidebarSegmented.astro +5 -5
  18. package/template/src/components/SidebarSubgroup.astro +35 -12
  19. package/template/src/components/TableOfContents.astro +24 -15
  20. package/template/src/components/ThemeSwitcher.astro +15 -8
  21. package/template/src/components/chat/AskAiWidget.tsx +10 -5
  22. package/template/src/components/endpoint/PlaygroundBar.astro +3 -3
  23. package/template/src/components/endpoint/PlaygroundButton.astro +3 -3
  24. package/template/src/components/endpoint/PlaygroundField.astro +53 -53
  25. package/template/src/components/endpoint/PlaygroundForm.astro +51 -37
  26. package/template/src/components/endpoint/RequestSnippets.astro +54 -21
  27. package/template/src/components/endpoint/ResponseDisplay.astro +24 -24
  28. package/template/src/components/endpoint/ResponseFieldTree.astro +12 -12
  29. package/template/src/components/endpoint/ResponseFields.astro +19 -19
  30. package/template/src/components/endpoint/ResponseSnippets.astro +66 -29
  31. package/template/src/components/sidebar/SidebarEndpointLink.astro +18 -15
  32. package/template/src/components/sidebar/SidebarOpenApiPageLink.astro +56 -0
  33. package/template/src/components/ui/CodeTabEdge.astro +6 -4
  34. package/template/src/components/ui/Field.astro +7 -7
  35. package/template/src/components/ui/Icon.astro +2 -1
  36. package/template/src/components/ui/demo/Demo.astro +1 -1
  37. package/template/src/components/user/Accordion.astro +3 -3
  38. package/template/src/components/user/Callout.astro +8 -8
  39. package/template/src/components/user/CodeBlock.astro +57 -22
  40. package/template/src/components/user/CodeGroup.astro +14 -10
  41. package/template/src/components/user/ComponentPreviewBlock.astro +38 -12
  42. package/template/src/components/user/Image.astro +6 -2
  43. package/template/src/components/user/Step.astro +4 -4
  44. package/template/src/components/user/Tab.astro +1 -1
  45. package/template/src/components/user/Tabs.astro +15 -20
  46. package/template/src/layouts/Layout.astro +9 -4
  47. package/template/src/lib/code/code-block.ts +150 -15
  48. package/template/src/lib/mdx/remark-resolve-internal-links.ts +639 -0
  49. package/template/src/lib/pagefind.ts +2 -1
  50. package/template/src/lib/routes.ts +134 -58
  51. package/template/src/lib/static-asset-url.ts +62 -0
  52. package/template/src/lib/utils.ts +48 -0
  53. package/template/src/lib/validation.ts +115 -27
  54. package/template/src/pages/404.astro +44 -0
  55. package/template/src/styles/global.css +28 -19
  56. package/template/scripts/rewrite-static-asset-host.mjs +0 -408
@@ -6,10 +6,10 @@
6
6
  "dev": "astro dev",
7
7
  "start": "tsx runner.ts",
8
8
  "prebuild": "rm -rf public/pagefind",
9
- "build": "astro build && node scripts/generate-og-metadata.mjs && node scripts/generate-og-images.mjs && node scripts/stamp-og-image-versions.mjs && node scripts/stamp-image-versions.mjs && pagefind --site dist && node scripts/stamp-pagefind-runtime-version.mjs && node scripts/rewrite-static-asset-host.mjs",
9
+ "build": "astro build && node scripts/generate-og-metadata.mjs && node scripts/generate-og-images.mjs && node scripts/stamp-og-image-versions.mjs && node scripts/stamp-image-versions.mjs && pagefind --site dist && node scripts/stamp-pagefind-runtime-version.mjs && node scripts/generate-proxy-allowed-origins.mjs && node scripts/generate-robots-txt.mjs",
10
10
  "preview": "astro preview",
11
11
  "astro": "astro",
12
- "search:index": "astro build && node scripts/generate-og-metadata.mjs && node scripts/generate-og-images.mjs && node scripts/stamp-og-image-versions.mjs && node scripts/stamp-image-versions.mjs && pagefind --site dist && node scripts/stamp-pagefind-runtime-version.mjs && node scripts/rewrite-static-asset-host.mjs && cp -r dist/pagefind public/"
12
+ "search:index": "astro build && node scripts/generate-og-metadata.mjs && node scripts/generate-og-images.mjs && node scripts/stamp-og-image-versions.mjs && node scripts/stamp-image-versions.mjs && pagefind --site dist && node scripts/stamp-pagefind-runtime-version.mjs && node scripts/generate-proxy-allowed-origins.mjs && node scripts/generate-robots-txt.mjs && cp -r dist/pagefind public/"
13
13
  },
14
14
  "dependencies": {
15
15
  "@alpinejs/collapse": "^3.15.2",
@@ -18,6 +18,7 @@
18
18
  "@astrojs/alpinejs": "^0.4.9",
19
19
  "@astrojs/mdx": "^4.3.12",
20
20
  "@astrojs/preact": "^4.1.3",
21
+ "@astrojs/sitemap": "^3.7.2",
21
22
  "@aws-sdk/client-s3": "^3.964.0",
22
23
  "@fontsource/google-sans-flex": "^5.2.2",
23
24
  "@iconify-json/lucide": "^1.2.79",
@@ -0,0 +1,217 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+
5
+ const CWD = process.cwd();
6
+ const DOCS_DIR = path.join(CWD, "src/content/docs");
7
+ const DOCS_CONFIG_PATH = path.join(DOCS_DIR, "docs.json");
8
+ const OUTPUT_DIR = path.join(CWD, ".radiant");
9
+ const OUTPUT_FILE = path.join(OUTPUT_DIR, "proxy-allowed-origins.json");
10
+
11
+ function isRecord(value) {
12
+ return typeof value === "object" && value !== null;
13
+ }
14
+
15
+ function isHttpUrl(value) {
16
+ return /^https?:\/\//i.test(String(value).trim());
17
+ }
18
+
19
+ function collectOpenApiSourcesFromNavigation(navigationNode, target) {
20
+ if (!isRecord(navigationNode)) {
21
+ return;
22
+ }
23
+
24
+ const openApiConfig = navigationNode.openapi;
25
+ if (typeof openApiConfig === "string") {
26
+ const trimmed = openApiConfig.trim();
27
+ if (trimmed) {
28
+ target.add(trimmed);
29
+ }
30
+ } else if (
31
+ isRecord(openApiConfig) &&
32
+ typeof openApiConfig.source === "string"
33
+ ) {
34
+ const trimmed = openApiConfig.source.trim();
35
+ if (trimmed) {
36
+ target.add(trimmed);
37
+ }
38
+ }
39
+
40
+ const menu = navigationNode.menu;
41
+ if (!isRecord(menu) || !Array.isArray(menu.items)) {
42
+ return;
43
+ }
44
+
45
+ for (const menuItem of menu.items) {
46
+ if (!isRecord(menuItem) || !isRecord(menuItem.submenu)) {
47
+ continue;
48
+ }
49
+ collectOpenApiSourcesFromNavigation(menuItem.submenu, target);
50
+ }
51
+ }
52
+
53
+ function resolveServerTemplateUrl(rawUrl, rawVariables) {
54
+ let unresolved = false;
55
+ const variables = isRecord(rawVariables) ? rawVariables : null;
56
+
57
+ const resolved = rawUrl.replace(/\{([^}]+)\}/g, (_match, tokenName) => {
58
+ if (!variables) {
59
+ unresolved = true;
60
+ return "";
61
+ }
62
+
63
+ const variableConfig = variables[tokenName];
64
+ if (
65
+ !isRecord(variableConfig) ||
66
+ typeof variableConfig.default !== "string"
67
+ ) {
68
+ unresolved = true;
69
+ return "";
70
+ }
71
+
72
+ const defaultValue = variableConfig.default.trim();
73
+ if (!defaultValue) {
74
+ unresolved = true;
75
+ return "";
76
+ }
77
+
78
+ return defaultValue;
79
+ });
80
+
81
+ if (unresolved) {
82
+ return null;
83
+ }
84
+
85
+ return resolved;
86
+ }
87
+
88
+ function normalizeAllowedOrigin(rawUrl) {
89
+ let parsed;
90
+ try {
91
+ parsed = new URL(rawUrl);
92
+ } catch {
93
+ return null;
94
+ }
95
+
96
+ if (parsed.protocol !== "https:") {
97
+ return null;
98
+ }
99
+
100
+ if (!parsed.hostname || parsed.username || parsed.password) {
101
+ return null;
102
+ }
103
+
104
+ return parsed.origin.toLowerCase();
105
+ }
106
+
107
+ async function loadOpenApiSpec(source) {
108
+ let fileContent;
109
+
110
+ if (isHttpUrl(source)) {
111
+ const response = await fetch(source);
112
+ if (!response.ok) {
113
+ throw new Error(
114
+ `Failed to fetch OpenAPI spec (${response.status} ${response.statusText})`,
115
+ );
116
+ }
117
+ fileContent = await response.text();
118
+ } else {
119
+ const absolutePath = path.join(DOCS_DIR, source);
120
+ fileContent = await fs.readFile(absolutePath, "utf8");
121
+ }
122
+
123
+ const trimmed = fileContent.trim();
124
+ if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html")) {
125
+ throw new Error("OpenAPI source returned HTML instead of JSON or YAML");
126
+ }
127
+
128
+ try {
129
+ return JSON.parse(fileContent);
130
+ } catch {
131
+ return YAML.parse(fileContent);
132
+ }
133
+ }
134
+
135
+ async function buildAllowedOrigins() {
136
+ const docsConfigRaw = await fs.readFile(DOCS_CONFIG_PATH, "utf8");
137
+ const docsConfig = JSON.parse(docsConfigRaw);
138
+
139
+ if (!isRecord(docsConfig) || !isRecord(docsConfig.navigation)) {
140
+ return [];
141
+ }
142
+
143
+ const sources = new Set();
144
+ collectOpenApiSourcesFromNavigation(docsConfig.navigation, sources);
145
+ if (sources.size === 0) {
146
+ return [];
147
+ }
148
+
149
+ const allowedOrigins = new Set();
150
+
151
+ for (const source of sources) {
152
+ try {
153
+ const spec = await loadOpenApiSpec(source);
154
+ const servers =
155
+ isRecord(spec) && Array.isArray(spec.servers) ? spec.servers : [];
156
+
157
+ for (const server of servers) {
158
+ if (!isRecord(server) || typeof server.url !== "string") {
159
+ continue;
160
+ }
161
+
162
+ const resolvedUrl = resolveServerTemplateUrl(
163
+ server.url,
164
+ server.variables,
165
+ );
166
+ if (!resolvedUrl) {
167
+ continue;
168
+ }
169
+
170
+ const normalizedOrigin = normalizeAllowedOrigin(resolvedUrl);
171
+ if (normalizedOrigin) {
172
+ allowedOrigins.add(normalizedOrigin);
173
+ }
174
+ }
175
+ } catch (error) {
176
+ console.warn(
177
+ `⚠️ Failed to extract OpenAPI server origins from "${source}":`,
178
+ error instanceof Error ? error.message : String(error),
179
+ );
180
+ }
181
+ }
182
+
183
+ return Array.from(allowedOrigins).sort();
184
+ }
185
+
186
+ async function run() {
187
+ let allowedOrigins = [];
188
+
189
+ try {
190
+ allowedOrigins = await buildAllowedOrigins();
191
+ } catch (error) {
192
+ console.warn(
193
+ "⚠️ Failed to generate proxy allowed origins. Falling back to empty allowlist.",
194
+ error instanceof Error ? error.message : String(error),
195
+ );
196
+ allowedOrigins = [];
197
+ }
198
+
199
+ await fs.mkdir(OUTPUT_DIR, { recursive: true });
200
+ await fs.writeFile(
201
+ OUTPUT_FILE,
202
+ `${JSON.stringify(allowedOrigins, null, 2)}\n`,
203
+ "utf8",
204
+ );
205
+
206
+ console.log(
207
+ `✅ Wrote ${allowedOrigins.length} proxy allowed origins to ${path.relative(CWD, OUTPUT_FILE)}`,
208
+ );
209
+ }
210
+
211
+ run().catch((error) => {
212
+ console.error(
213
+ "❌ Failed to write proxy allowed origins manifest:",
214
+ error instanceof Error ? error.message : String(error),
215
+ );
216
+ process.exit(1);
217
+ });
@@ -0,0 +1,19 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const CWD = process.cwd();
5
+ const DIST_DIR = path.join(CWD, "dist");
6
+ const ROBOTS_TXT_PATH = path.join(DIST_DIR, "robots.txt");
7
+
8
+ function main() {
9
+ if (!fs.existsSync(DIST_DIR)) {
10
+ console.log("Skipping robots.txt generation: dist directory not found.");
11
+ return;
12
+ }
13
+
14
+ const robotsTxt = "User-agent: *\nAllow: /\nSitemap: /sitemap-index.xml\n";
15
+ fs.writeFileSync(ROBOTS_TXT_PATH, robotsTxt, "utf8");
16
+ console.log("✅ robots.txt generated.");
17
+ }
18
+
19
+ main();
@@ -6,6 +6,28 @@ const CWD = process.cwd();
6
6
  const DIST_DIR = path.join(CWD, "dist");
7
7
  const VERSION_LENGTH = 12;
8
8
  const DIST_ROOT = path.resolve(DIST_DIR);
9
+ const CONFIGURED_STATIC_HOST = (() => {
10
+ const raw =
11
+ typeof process.env.STATIC_ASSET_HOST === "string"
12
+ ? process.env.STATIC_ASSET_HOST.trim()
13
+ : "";
14
+ if (!raw) return null;
15
+ const withScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(raw)
16
+ ? raw
17
+ : `https://${raw}`;
18
+
19
+ try {
20
+ const parsed = new URL(withScheme);
21
+ return parsed.hostname.toLowerCase();
22
+ } catch {
23
+ return null;
24
+ }
25
+ })();
26
+ const CONFIGURED_PREFIX =
27
+ typeof process.env.R2_BUCKET_PREFIX === "string" &&
28
+ process.env.R2_BUCKET_PREFIX.trim().length > 0
29
+ ? process.env.R2_BUCKET_PREFIX.trim().replace(/^\/+|\/+$/g, "")
30
+ : null;
9
31
  const VERSIONED_EXTENSIONS = new Set([
10
32
  ".avif",
11
33
  ".gif",
@@ -76,6 +98,24 @@ function getHash(filePath) {
76
98
  return hash;
77
99
  }
78
100
 
101
+ function normalizePathForDistLookup(pathname) {
102
+ const normalizedPathname = path.posix.normalize(pathname);
103
+ if (!CONFIGURED_PREFIX) {
104
+ return normalizedPathname;
105
+ }
106
+
107
+ const prefix = `/${CONFIGURED_PREFIX}`;
108
+ if (
109
+ normalizedPathname === prefix ||
110
+ normalizedPathname.startsWith(`${prefix}/`)
111
+ ) {
112
+ const stripped = normalizedPathname.slice(prefix.length);
113
+ return stripped.startsWith("/") ? stripped : `/${stripped}`;
114
+ }
115
+
116
+ return normalizedPathname;
117
+ }
118
+
79
119
  function resolveLocalImagePath(urlValue, filePath) {
80
120
  const trimmed = urlValue.trim();
81
121
  if (!trimmed) return null;
@@ -91,11 +131,6 @@ function resolveLocalImagePath(urlValue, filePath) {
91
131
  return null;
92
132
  }
93
133
 
94
- // Ignore fully-qualified URLs.
95
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(decoded)) {
96
- return null;
97
- }
98
-
99
134
  let parsed;
100
135
  try {
101
136
  parsed = new URL(decoded, `https://radiant.invalid${htmlBasePathname(filePath)}`);
@@ -103,13 +138,27 @@ function resolveLocalImagePath(urlValue, filePath) {
103
138
  return null;
104
139
  }
105
140
 
141
+ const isAbsoluteUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(decoded);
142
+ if (isAbsoluteUrl) {
143
+ if (!CONFIGURED_STATIC_HOST) {
144
+ return null;
145
+ }
146
+
147
+ if (parsed.protocol !== "https:") {
148
+ return null;
149
+ }
150
+
151
+ if (parsed.hostname.toLowerCase() !== CONFIGURED_STATIC_HOST) {
152
+ return null;
153
+ }
154
+ }
155
+
106
156
  const extension = path.extname(parsed.pathname).toLowerCase();
107
157
  if (!VERSIONED_EXTENSIONS.has(extension)) {
108
158
  return null;
109
159
  }
110
160
 
111
- const normalizedPathname = path.posix
112
- .normalize(parsed.pathname)
161
+ const normalizedPathname = normalizePathForDistLookup(parsed.pathname)
113
162
  .replace(/^\/+/, "");
114
163
  const absolutePath = path.resolve(DIST_DIR, normalizedPathname);
115
164
 
@@ -124,12 +173,15 @@ function resolveLocalImagePath(urlValue, filePath) {
124
173
  return null;
125
174
  }
126
175
 
127
- return { parsed, absolutePath };
176
+ return { parsed, absolutePath, isAbsoluteUrl };
128
177
  }
129
178
 
130
- function formatUpdatedUrl(parsed) {
179
+ function formatUpdatedUrl(parsed, isAbsoluteUrl) {
131
180
  const pathnameWithParams = `${parsed.pathname}${parsed.search}${parsed.hash}`;
132
- return pathnameWithParams.replace(/&/g, "&amp;");
181
+ const fullValue = isAbsoluteUrl
182
+ ? `${parsed.origin}${pathnameWithParams}`
183
+ : pathnameWithParams;
184
+ return fullValue.replace(/&/g, "&amp;");
133
185
  }
134
186
 
135
187
  function stampSingleUrl(urlValue, filePath) {
@@ -141,7 +193,7 @@ function stampSingleUrl(urlValue, filePath) {
141
193
  const version = getHash(resolved.absolutePath);
142
194
  resolved.parsed.searchParams.set("v", version);
143
195
 
144
- const updated = formatUpdatedUrl(resolved.parsed);
196
+ const updated = formatUpdatedUrl(resolved.parsed, resolved.isAbsoluteUrl);
145
197
  return { value: updated, changed: updated !== urlValue };
146
198
  }
147
199
 
@@ -91,7 +91,7 @@ const socialIcons: Record<string, string> = {
91
91
  href="https://radiant.io"
92
92
  class:list={[
93
93
  "group flex items-center gap-1.5 text-[13px] text-neutral-400 dark:text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-300 transition-colors duration-200",
94
- askAiEnabled && "sm:pr-16",
94
+ askAiEnabled && "sm:pr-13",
95
95
  ]}
96
96
  >
97
97
  Built with <span
@@ -15,9 +15,9 @@ const config = await getConfig();
15
15
 
16
16
  <header
17
17
  class:list={[
18
- "fixed z-30 top-1 inset-x-1 shadow-[0px_-20px_0px_20px_var(--color-neutral-100)] h-16 border-x border-t border-border rounded-t-2xl overflow-hidden border-b bg-background border-b-border-light",
18
+ "fixed z-30 top-1 inset-x-1 shadow-[0px_-20px_0px_20px_var(--background-dark)] h-16 border-x border-t border-border rounded-t-2xl overflow-hidden border-b bg-background border-b-border-light",
19
19
  config.navbar?.blur &&
20
- "sm:bg-background/80 sm:backdrop-blur-[18px] sm:backdrop-saturate-50 sm:border-b-neutral-100/85 sm:bg-clip-padding",
20
+ "sm:bg-background/80 sm:backdrop-blur-[18px] sm:backdrop-saturate-50 sm:border-b-neutral-100/85 sm:dark:border-neutral-800/85 sm:bg-clip-padding",
21
21
  ]}
22
22
  data-pagefind-ignore
23
23
  data-vaul-scale-chrome
@@ -34,12 +34,12 @@ const config = await getConfig();
34
34
  x-bind:class="open ? 'rotate-90': ''"
35
35
  >
36
36
  <div
37
- class="w-[14px] h-0.5 bg-neutral-900 rounded-full origin-[20%_50%] transition"
37
+ class="w-[14px] h-0.5 bg-neutral-900 dark:bg-white rounded-full origin-[20%_50%] transition"
38
38
  x-bind:class="open ? 'rotate-45': ''"
39
39
  >
40
40
  </div>
41
41
  <div
42
- class="w-[14px] h-0.5 bg-neutral-900 rounded-full origin-[20%_50%] transition"
42
+ class="w-[14px] h-0.5 bg-neutral-900 dark:bg-white rounded-full origin-[20%_50%] transition"
43
43
  x-bind:class="open ? '-rotate-45': ''"
44
44
  >
45
45
  </div>
@@ -58,14 +58,14 @@ const config = await getConfig();
58
58
  showAskAiTrigger ? (
59
59
  <button
60
60
  type="button"
61
- class="hidden md:inline-flex items-center gap-1.5 h-8 rounded-lg [corner-shape:superellipse(1.2)] bg-linear-to-b from-neutral-900/85 to-neutral-900 px-3 text-xs text-white shadow-sm dark:bg-white dark:text-neutral-900 cursor-pointer"
61
+ class="hidden md:inline-flex items-center gap-2 h-8 rounded-lg [corner-shape:superellipse(1.2)] bg-linear-to-b from-neutral-900/85 dark:from-neutral-100 to-neutral-900 dark:to-neutral-200 px-3 text-[13px] font-[350] dark:font-medium text-white shadow-sm dark:bg-white dark:text-neutral-900 cursor-pointer"
62
62
  onclick="window.dispatchEvent(new CustomEvent('ask-ai:open'));"
63
63
  >
64
64
  <img
65
65
  src={sparkleIcon}
66
66
  alt=""
67
67
  aria-hidden="true"
68
- class="size-3 invert dark:invert-0"
68
+ class="size-3 -ml-px invert dark:invert-0"
69
69
  />
70
70
  Ask AI
71
71
  </button>
@@ -87,7 +87,7 @@ const config = await getConfig();
87
87
  {config.navbar.links.map((l, i, a) => (
88
88
  <a
89
89
  class:list={[
90
- "items-center gap-1 text-[13px] font-[450] text-neutral-600/85 hover:text-neutral-600 duration-200 px-1.5 py-[5px] whitespace-nowrap",
90
+ "items-center gap-1 text-[13px] font-[450] dark:font-normal text-neutral-600/85 hover:text-neutral-600 dark:text-neutral-200/90 dark:hover:text-neutral-200 duration-200 px-1.5 py-[5px] whitespace-nowrap",
91
91
  showAskAiTrigger && a.length === 3 && i === 2
92
92
  ? "hidden 2xl:flex"
93
93
  : "flex",
@@ -110,7 +110,7 @@ const config = await getConfig();
110
110
  {config.navbar.secondary && (
111
111
  <a
112
112
  class:list={[
113
- "h-[33px] items-center gap-1.5 px-3 text-[13px] bg-white/90 text-neutral-600/85 hover:text-neutral-600 rounded-lg [corner-shape:superellipse(1.2)] border border-neutral-200 shadow-xs hover:shadow-sm. transition-all whitespace-nowrap",
113
+ "h-[33px] items-center gap-1.5 px-[11px] text-[13px] bg-white/90 dark:bg-neutral-800/80 text-neutral-600/85 hover:text-neutral-600 dark:text-neutral-200/95 dark:hover:text-neutral-200 rounded-lg [corner-shape:superellipse(1.2)] border border-neutral-200 dark:border-neutral-700/40 shadow-xs transition-all whitespace-nowrap",
114
114
  config.navbar.primary ? "hidden lg:flex" : "flex",
115
115
  ]}
116
116
  href={config.navbar.secondary.href}
@@ -129,7 +129,7 @@ const config = await getConfig();
129
129
  {config.navbar.primary && (
130
130
  <a
131
131
  class:list={[
132
- "h-8 flex items-center gap-2 px-3 text-[13px] rounded-lg [corner-shape:superellipse(1.2)] bg-linear-to-b from-neutral-900/85 to-neutral-900 text-white duration-200 shadow-sm transition-all whitespace-nowrap",
132
+ "font-[350] dark:font-[450] h-8 flex items-center gap-2 px-3 text-[13px] rounded-lg [corner-shape:superellipse(1.2)] bg-linear-to-b from-neutral-900/85 to-neutral-900 dark:from-neutral-100 dark:to-neutral-200 text-white dark:text-neutral-950 duration-200 shadow-sm transition-all whitespace-nowrap",
133
133
  ]}
134
134
  href={config.navbar.primary.href}
135
135
  >
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import { getConfig, type LogoVariant } from "../lib/validation";
3
+ import { resolveStaticAssetUrl } from "../lib/static-asset-url";
3
4
 
4
5
  const config = await getConfig();
5
6
 
@@ -12,7 +13,7 @@ function getLogoUrl(logoPath: string | undefined): string | null {
12
13
  : logoPath;
13
14
 
14
15
  // Return public URL - Vite plugin ensures file exists in public
15
- return `/${normalizedPath}`;
16
+ return resolveStaticAssetUrl(`/${normalizedPath}`);
16
17
  }
17
18
 
18
19
  function resolveLogoVariant(
@@ -908,7 +908,7 @@ const formattedBodyDescription = bodyDescription
908
908
  {
909
909
  formattedDescription && (
910
910
  <div
911
- class="mb-6 prose-rules text-neutral-500 **:text-neutral-500"
911
+ class="mb-6 prose-rules text-neutral-500 **:text-neutral-500 dark:text-neutral-400 dark:**:text-neutral-400"
912
912
  set:html={formattedDescription}
913
913
  />
914
914
  )
@@ -952,31 +952,31 @@ const formattedBodyDescription = bodyDescription
952
952
  <h4 class="text-xl font-semibold">{headers[key]}</h4>
953
953
  {key === "body" && formattedBodyDescription && (
954
954
  <div
955
- class="mt-2 prose-rules prose-sm! text-neutral-500 **:text-neutral-500"
955
+ class="mt-2 prose-rules prose-sm! text-neutral-500 **:text-neutral-500 dark:text-neutral-400 dark:**:text-neutral-400"
956
956
  set:html={formattedBodyDescription}
957
957
  />
958
958
  )}
959
959
  {hasCommonFields && (
960
960
  <div x-data="{ expanded: false }" class="mt-4">
961
961
  <div
962
- class="w-full overflow-hidden rounded-xl border border-neutral-200 bg-white transition-colors duration-200"
963
- x-bind:class="expanded ? 'border-neutral-300' : 'border-neutral-200'"
962
+ class="w-full overflow-hidden rounded-xl border border-neutral-200 bg-white transition-colors duration-200 dark:border-neutral-800 dark:bg-(--rd-code-surface)"
963
+ x-bind:class="expanded ? 'border-neutral-300 dark:border-neutral-700' : 'border-neutral-200 dark:border-neutral-800'"
964
964
  >
965
965
  <button
966
966
  type="button"
967
967
  x-on:click="expanded = !expanded"
968
968
  x-bind:aria-expanded="expanded"
969
- class="group flex w-full items-center justify-between gap-3 px-4 py-2.5 text-left text-sm font-medium text-neutral-700 hover:bg-neutral-50 cursor-pointer transition-colors duration-200"
969
+ class="group flex w-full items-center justify-between gap-3 px-4 py-2.5 text-left text-sm font-medium text-neutral-700 hover:bg-neutral-50 cursor-pointer transition-colors duration-200 dark:text-neutral-300 dark:hover:bg-neutral-800/60"
970
970
  >
971
971
  <span class="inline-flex items-center gap-2">
972
- <ListChevronsToggle class="size-4 shrink-0 text-neutral-400 group-hover:text-neutral-600 transition duration-200" />
972
+ <ListChevronsToggle class="size-4 shrink-0 text-neutral-400 group-hover:text-neutral-600 transition duration-200 dark:text-neutral-500 dark:group-hover:text-neutral-300" />
973
973
  <span>
974
974
  {sectionFields.length} {noun}
975
975
  </span>
976
976
  </span>
977
977
  </button>
978
978
  <div x-show="expanded" x-collapse x-cloak>
979
- <div class="border-t border-neutral-100 p-4">
979
+ <div class="border-t border-neutral-100 p-4 dark:border-neutral-800">
980
980
  <ResponseFieldTree fields={sectionFields} />
981
981
  </div>
982
982
  </div>
@@ -990,30 +990,30 @@ const formattedBodyDescription = bodyDescription
990
990
  hasCommonFields ? "mt-3" : "mt-4",
991
991
  ]}
992
992
  >
993
- <p class="text-xs text-neutral-500">
993
+ <p class="text-xs text-neutral-500 dark:text-neutral-400">
994
994
  {sectionVariantData?.variantType === "anyOf"
995
995
  ? "One or more variants may apply."
996
996
  : "One of these variants applies."}
997
997
  </p>
998
998
  {sectionVariants.map((variant, index) => (
999
999
  <>
1000
- <div class="rounded-lg border border-neutral-200 bg-white p-3">
1001
- <div class="mb-2 text-xs font-medium text-neutral-600">
1000
+ <div class="rounded-lg border border-neutral-200 bg-white p-3 dark:border-neutral-800 dark:bg-neutral-900/60">
1001
+ <div class="mb-2 text-xs font-medium text-neutral-600 dark:text-neutral-400">
1002
1002
  {variant.label}
1003
1003
  </div>
1004
1004
  <div x-data="{ expanded: false }">
1005
1005
  <div
1006
- class="w-full overflow-hidden rounded-lg border border-neutral-200 bg-white transition-colors duration-200"
1007
- x-bind:class="expanded ? 'border-neutral-300' : 'border-neutral-200'"
1006
+ class="w-full overflow-hidden rounded-lg border border-neutral-200 bg-white transition-colors duration-200 dark:border-neutral-800 dark:bg-(--rd-code-surface)"
1007
+ x-bind:class="expanded ? 'border-neutral-300 dark:border-neutral-700' : 'border-neutral-200 dark:border-neutral-800'"
1008
1008
  >
1009
1009
  <button
1010
1010
  type="button"
1011
1011
  x-on:click="expanded = !expanded"
1012
1012
  x-bind:aria-expanded="expanded"
1013
- class="group flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-xs font-medium text-neutral-700 hover:bg-neutral-50 cursor-pointer transition-colors duration-200"
1013
+ class="group flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-xs font-medium text-neutral-700 hover:bg-neutral-50 cursor-pointer transition-colors duration-200 dark:text-neutral-300 dark:hover:bg-neutral-800/60"
1014
1014
  >
1015
1015
  <span class="inline-flex items-center gap-2">
1016
- <ListChevronsToggle class="size-4 shrink-0 text-neutral-400 group-hover:text-neutral-600 transition duration-200" />
1016
+ <ListChevronsToggle class="size-4 shrink-0 text-neutral-400 group-hover:text-neutral-600 transition duration-200 dark:text-neutral-500 dark:group-hover:text-neutral-300" />
1017
1017
  <span>
1018
1018
  {variant.fields.length}{" "}
1019
1019
  {variant.fields.length === 1
@@ -1023,7 +1023,7 @@ const formattedBodyDescription = bodyDescription
1023
1023
  </span>
1024
1024
  </button>
1025
1025
  <div x-show="expanded" x-collapse x-cloak>
1026
- <div class="border-t border-neutral-100 px-3 py-3">
1026
+ <div class="border-t border-neutral-100 px-3 py-3 dark:border-neutral-800">
1027
1027
  <ResponseFieldTree
1028
1028
  fields={variant.fields}
1029
1029
  />
@@ -1035,11 +1035,11 @@ const formattedBodyDescription = bodyDescription
1035
1035
  {sectionVariantData?.variantType === "oneOf" &&
1036
1036
  index < sectionVariants.length - 1 && (
1037
1037
  <div class="flex items-center gap-2 py-0">
1038
- <div class="h-px flex-1 bg-neutral-200" />
1039
- <span class="px-1 text-[10px] uppercase tracking-wide text-neutral-500">
1038
+ <div class="h-px flex-1 bg-neutral-200 dark:bg-neutral-800" />
1039
+ <span class="px-1 text-[10px] uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
1040
1040
  OR
1041
1041
  </span>
1042
- <div class="h-px flex-1 bg-neutral-200" />
1042
+ <div class="h-px flex-1 bg-neutral-200 dark:bg-neutral-800" />
1043
1043
  </div>
1044
1044
  )}
1045
1045
  </>