radiant-docs 0.1.39 → 0.1.41

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 (49) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +38 -7
  3. package/template/package-lock.json +19 -7
  4. package/template/package.json +3 -3
  5. package/template/public/favicon.svg +16 -8
  6. package/template/scripts/generate-robots-txt.mjs +29 -1
  7. package/template/scripts/remove-assistant-for-non-pro.mjs +28 -0
  8. package/template/scripts/stamp-image-versions.mjs +59 -33
  9. package/template/src/components/Footer.astro +2 -1
  10. package/template/src/components/Header.astro +10 -8
  11. package/template/src/components/LogoLink.astro +2 -1
  12. package/template/src/components/MdxPage.astro +15 -4
  13. package/template/src/components/PagePagination.astro +61 -0
  14. package/template/src/components/SidebarDropdown.astro +12 -8
  15. package/template/src/components/SidebarGroup.astro +1 -1
  16. package/template/src/components/SidebarMenu.astro +1 -1
  17. package/template/src/components/SidebarSegmented.astro +6 -5
  18. package/template/src/components/TableOfContents.astro +4 -13
  19. package/template/src/components/chat/AskAiWidget.tsx +274 -39
  20. package/template/src/components/chat/AssistantDocsWidget.astro +16 -0
  21. package/template/src/components/chat/AssistantDocsWidget.tsx +402 -0
  22. package/template/src/components/chat/AssistantEmbedPanel.tsx +1693 -0
  23. package/template/src/components/chat/AssistantEmbedPanelPage.astro +95 -0
  24. package/template/src/components/endpoint/PlaygroundForm.astro +2 -1
  25. package/template/src/components/user/Callout.astro +10 -4
  26. package/template/src/components/user/CodeBlock.astro +1 -1
  27. package/template/src/components/user/CodeGroup.astro +16 -1
  28. package/template/src/components/user/ComponentPreviewBlock.astro +1 -0
  29. package/template/src/components/user/Image.astro +43 -53
  30. package/template/src/layouts/Layout.astro +104 -35
  31. package/template/src/lib/assistant-chrome-defaults.ts +74 -0
  32. package/template/src/lib/assistant-chrome.ts +39 -0
  33. package/template/src/lib/assistant-embed-script.ts +897 -0
  34. package/template/src/lib/assistant-panel-config.ts +80 -0
  35. package/template/src/lib/base-path.ts +98 -0
  36. package/template/src/lib/component-error.ts +49 -10
  37. package/template/src/lib/favicon.ts +31 -0
  38. package/template/src/lib/mdx/remark-resolve-internal-links.ts +128 -18
  39. package/template/src/lib/pagefind.ts +62 -14
  40. package/template/src/lib/routes.ts +49 -1
  41. package/template/src/lib/static-asset-url.ts +3 -1
  42. package/template/src/lib/theme-css.ts +176 -0
  43. package/template/src/lib/utils.ts +12 -4
  44. package/template/src/lib/validation.ts +754 -37
  45. package/template/src/pages/-/assistant/embed.js.ts +15 -0
  46. package/template/src/pages/-/assistant/panel.astro +5 -0
  47. package/template/src/pages/404.astro +6 -5
  48. package/template/src/pages/[...slug].astro +68 -6
  49. package/template/src/styles/global.css +62 -1
@@ -5,6 +5,7 @@ import {
5
5
  type NavOpenApiPage,
6
6
  type NavMenuItem,
7
7
  type NavOpenApi,
8
+ type HiddenPageRoute,
8
9
  loadOpenApiSpec,
9
10
  } from "./validation";
10
11
  import {
@@ -21,6 +22,7 @@ type MdxNavPageItem = string | NavPage;
21
22
  export interface BaseRoute {
22
23
  slug: string;
23
24
  title: string;
25
+ hidden?: boolean;
24
26
  }
25
27
 
26
28
  // MDX route
@@ -95,6 +97,35 @@ function processPageItem(
95
97
  };
96
98
  }
97
99
 
100
+ function processHiddenPageRoute(
101
+ route: HiddenPageRoute,
102
+ docs: any[],
103
+ ): MdxRoute {
104
+ const entry = docs.find((doc: any) => {
105
+ const docPath = doc.id.replace(/\.mdx$/, "");
106
+ return docPath === route.filePath;
107
+ });
108
+
109
+ if (!entry) {
110
+ throw new Error(
111
+ `Could not find content collection entry for path: ${route.filePath}`,
112
+ );
113
+ }
114
+
115
+ const slug = route.href.replace(/^\/+/, "").replace(/\/+$/, "");
116
+
117
+ return {
118
+ type: "mdx",
119
+ slug,
120
+ filePath: route.filePath,
121
+ title: resolveMdxPageTitle({
122
+ entry,
123
+ filePath: route.filePath,
124
+ }),
125
+ hidden: true,
126
+ };
127
+ }
128
+
98
129
  type OpenApiOperationLookup = {
99
130
  method: string;
100
131
  path: string;
@@ -368,7 +399,7 @@ function assertUniqueRouteSlugs(routes: Route[]): void {
368
399
  : `openapi:${route.filePath}:${route.openApiMethod.toUpperCase()} ${route.openApiPath}`;
369
400
 
370
401
  throw new Error(
371
- `Duplicate route slug "${route.slug}" generated by "${existingLabel}" and "${candidateLabel}".`,
402
+ `[USER_ERROR]: Invalid docs.json: Duplicate route slug "${route.slug}" generated by "${existingLabel}" and "${candidateLabel}". Remove one of the duplicate references or change navigation structure so each route resolves to a unique URL.`,
372
403
  );
373
404
  }
374
405
  }
@@ -405,6 +436,23 @@ export async function getAllRoutes(): Promise<Route[]> {
405
436
  }
406
437
  }
407
438
 
439
+ for (const hiddenPageRoute of config.hiddenPageRoutes ?? []) {
440
+ const hiddenRoute = processHiddenPageRoute(hiddenPageRoute, docs);
441
+ const existingRoute = allRoutes.find(
442
+ (route) => route.slug === hiddenRoute.slug,
443
+ );
444
+ if (existingRoute) {
445
+ if (
446
+ existingRoute.type === "mdx" &&
447
+ existingRoute.filePath === hiddenRoute.filePath
448
+ ) {
449
+ continue;
450
+ }
451
+ }
452
+
453
+ allRoutes.push(hiddenRoute);
454
+ }
455
+
408
456
  assertUniqueRouteSlugs(allRoutes);
409
457
  return allRoutes;
410
458
  }
@@ -1,3 +1,5 @@
1
+ import { withBasePath } from "./base-path";
2
+
1
3
  type AssetsPrefixValue = string | Record<string, string> | undefined;
2
4
 
3
5
  function normalizePrefix(value: string): string {
@@ -54,7 +56,7 @@ export function resolveStaticAssetUrl(rawPath: string): string {
54
56
 
55
57
  const prefix = resolveAssetsPrefix(import.meta.env.ASSETS_PREFIX);
56
58
  if (!prefix) {
57
- return `${normalizedPathname}${parsed.search}${parsed.hash}`;
59
+ return `${withBasePath(normalizedPathname)}${parsed.search}${parsed.hash}`;
58
60
  }
59
61
 
60
62
  const normalizedPrefixPath = `${prefix}/${normalizedPathname.replace(/^\/+/, "")}`;
@@ -0,0 +1,176 @@
1
+ import colors from "tailwindcss/colors";
2
+ import {
3
+ DEFAULT_THEME_COLOR_DARK,
4
+ DEFAULT_THEME_COLOR_LIGHT,
5
+ type BaseColorOption,
6
+ type DocsConfig,
7
+ } from "./validation";
8
+
9
+ const neutralColorShades = [
10
+ "50",
11
+ "100",
12
+ "200",
13
+ "300",
14
+ "400",
15
+ "500",
16
+ "600",
17
+ "700",
18
+ "800",
19
+ "900",
20
+ "950",
21
+ ] as const;
22
+ type NeutralColorShade = (typeof neutralColorShades)[number];
23
+
24
+ const paletteColors = colors as unknown as Record<
25
+ string,
26
+ Record<NeutralColorShade, string>
27
+ >;
28
+
29
+ function normalizeHexColorToRgb(
30
+ hexColor: string,
31
+ ): { r: number; g: number; b: number } | null {
32
+ const normalized = hexColor.replace("#", "").trim();
33
+ if (/^[a-fA-F0-9]{3,4}$/.test(normalized)) {
34
+ const [r, g, b] = normalized.split("");
35
+ if (!r || !g || !b) return null;
36
+ return {
37
+ r: Number.parseInt(`${r}${r}`, 16),
38
+ g: Number.parseInt(`${g}${g}`, 16),
39
+ b: Number.parseInt(`${b}${b}`, 16),
40
+ };
41
+ }
42
+
43
+ if (/^[a-fA-F0-9]{6,8}$/.test(normalized)) {
44
+ return {
45
+ r: Number.parseInt(normalized.slice(0, 2), 16),
46
+ g: Number.parseInt(normalized.slice(2, 4), 16),
47
+ b: Number.parseInt(normalized.slice(4, 6), 16),
48
+ };
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ function getOklabLightness(color: string): number | null {
55
+ const match = color
56
+ .trim()
57
+ .match(/^okl(?:ab|ch)\(\s*([+-]?(?:\d+\.?\d*|\.\d+))(%?)/i);
58
+ if (!match?.[1]) return null;
59
+
60
+ const lightness = Number.parseFloat(match[1]);
61
+ if (!Number.isFinite(lightness)) return null;
62
+
63
+ const normalizedLightness =
64
+ match[2] === "%" || lightness > 1 ? lightness / 100 : lightness;
65
+ return Math.min(Math.max(normalizedLightness, 0), 1);
66
+ }
67
+
68
+ export function getThemeForegroundColor(
69
+ color: string,
70
+ darkForeground = "#111827",
71
+ ): string {
72
+ const rgb = normalizeHexColorToRgb(color);
73
+ const oklabLightness = getOklabLightness(color);
74
+ if (!rgb && oklabLightness !== null) {
75
+ return oklabLightness > 0.6 ? darkForeground : "#ffffff";
76
+ }
77
+
78
+ if (!rgb) return "#ffffff";
79
+
80
+ const toLinear = (channel: number): number => {
81
+ const normalized = channel / 255;
82
+ return normalized <= 0.03928
83
+ ? normalized / 12.92
84
+ : ((normalized + 0.055) / 1.055) ** 2.4;
85
+ };
86
+
87
+ const luminance =
88
+ 0.2126 * toLinear(rgb.r) +
89
+ 0.7152 * toLinear(rgb.g) +
90
+ 0.0722 * toLinear(rgb.b);
91
+ return luminance > 0.45 ? darkForeground : "#ffffff";
92
+ }
93
+
94
+ function getThemeColorVariables(themeColor: string): string[] {
95
+ const foreground = getThemeForegroundColor(themeColor);
96
+ const iconFilter = foreground === "#ffffff" ? "invert(1)" : "none";
97
+
98
+ return [
99
+ `--color-theme: ${themeColor};`,
100
+ `--color-theme-foreground: ${foreground};`,
101
+ `--color-theme-icon-filter: ${iconFilter};`,
102
+ "--color-theme-top: color-mix(in oklab, var(--color-theme) 88%, white);",
103
+ "--color-theme-bottom: color-mix(in oklab, var(--color-theme) 90%, black);",
104
+ ];
105
+ }
106
+
107
+ function getNeutralPaletteVariables(baseColor: BaseColorOption): string[] {
108
+ const palette = paletteColors[baseColor] ?? paletteColors.neutral;
109
+ return [
110
+ `--color-neutral: ${palette["500"]};`,
111
+ ...neutralColorShades.map(
112
+ (shade) => `--color-neutral-${shade}: ${palette[shade]};`,
113
+ ),
114
+ ];
115
+ }
116
+
117
+ export function getDocsBaseColorShade(
118
+ theme: DocsConfig["theme"],
119
+ mode: "light" | "dark",
120
+ shade: NeutralColorShade,
121
+ ): string {
122
+ const themeBaseColor = theme?.baseColor ?? "neutral";
123
+ const baseColor: BaseColorOption =
124
+ typeof themeBaseColor === "string" ? themeBaseColor : themeBaseColor[mode];
125
+ const palette = paletteColors[baseColor] ?? paletteColors.neutral;
126
+ return palette[shade];
127
+ }
128
+
129
+ export function getDocsLightBaseColorShade(
130
+ theme: DocsConfig["theme"],
131
+ shade: NeutralColorShade,
132
+ ): string {
133
+ return getDocsBaseColorShade(theme, "light", shade);
134
+ }
135
+
136
+ export function getDocsDarkBaseColorShade(
137
+ theme: DocsConfig["theme"],
138
+ shade: NeutralColorShade,
139
+ ): string {
140
+ return getDocsBaseColorShade(theme, "dark", shade);
141
+ }
142
+
143
+ export function getDocsThemeCss(theme: DocsConfig["theme"]): string {
144
+ const themeBaseColor = theme?.baseColor ?? "neutral";
145
+ const themeThemeColor = theme?.themeColor;
146
+ const lightBaseColor: BaseColorOption =
147
+ typeof themeBaseColor === "string" ? themeBaseColor : themeBaseColor.light;
148
+ const darkBaseColor: BaseColorOption =
149
+ typeof themeBaseColor === "string" ? themeBaseColor : themeBaseColor.dark;
150
+ const lightThemeColor =
151
+ typeof themeThemeColor === "string"
152
+ ? themeThemeColor
153
+ : (themeThemeColor?.light ?? DEFAULT_THEME_COLOR_LIGHT);
154
+ const darkThemeColor =
155
+ typeof themeThemeColor === "string"
156
+ ? themeThemeColor
157
+ : (themeThemeColor?.dark ?? DEFAULT_THEME_COLOR_DARK);
158
+
159
+ const lightNeutralVariables = getNeutralPaletteVariables(lightBaseColor);
160
+ const darkNeutralVariables = getNeutralPaletteVariables(darkBaseColor);
161
+ const lightThemeColorVariables = getThemeColorVariables(lightThemeColor);
162
+ const darkThemeColorVariables = getThemeColorVariables(darkThemeColor);
163
+
164
+ return [
165
+ "html[data-theme='light'], html:not(.dark):not([data-theme='dark']) {",
166
+ ...[...lightNeutralVariables, ...lightThemeColorVariables].map(
167
+ (declaration) => ` ${declaration.replace(/;$/, " !important;")}`,
168
+ ),
169
+ "}",
170
+ "html.dark, html[data-theme='dark'] {",
171
+ ...[...darkNeutralVariables, ...darkThemeColorVariables].map(
172
+ (declaration) => ` ${declaration.replace(/;$/, " !important;")}`,
173
+ ),
174
+ "}",
175
+ ].join("\n");
176
+ }
@@ -5,6 +5,7 @@ import remarkRehype from "remark-rehype";
5
5
  import rehypeStringify from "rehype-stringify";
6
6
  import rehypeExternalLinks from "./mdx/rehype-external-links";
7
7
  import path from "node:path";
8
+ import { prependBasePath } from "./base-path";
8
9
 
9
10
  export function slugify(text: string): string {
10
11
  if (typeof text !== "string") {
@@ -66,7 +67,7 @@ export function buildMdxPageHref(args: {
66
67
  homePath?: string;
67
68
  }): string {
68
69
  if (args.homePath && args.filePath === args.homePath) {
69
- return "/";
70
+ return prependBasePath("/");
70
71
  }
71
72
 
72
73
  const filename = path.basename(args.filePath);
@@ -75,9 +76,11 @@ export function buildMdxPageHref(args: {
75
76
  .replace(/^\/+/, "")
76
77
  .replace(/\/+$/, "");
77
78
 
78
- return normalizedGroupSlug
79
+ const href = normalizedGroupSlug
79
80
  ? `/${normalizedGroupSlug}/${pageSlug}`
80
81
  : `/${pageSlug}`;
82
+
83
+ return prependBasePath(href);
81
84
  }
82
85
 
83
86
  export function parseOpenApiEndpoint(
@@ -103,7 +106,10 @@ export function parseOpenApiEndpoint(
103
106
  };
104
107
  }
105
108
 
106
- export function buildOpenApiEndpointSlug(pathStr: string, method: string): string {
109
+ export function buildOpenApiEndpointSlug(
110
+ pathStr: string,
111
+ method: string,
112
+ ): string {
107
113
  const normalizedPath = pathStr.startsWith("/") ? pathStr : `/${pathStr}`;
108
114
  const pathSlug = normalizedPath
109
115
  .replace(/^\//, "")
@@ -123,7 +129,9 @@ export function buildOpenApiEndpointHref(args: {
123
129
  .replace(/^\/+/, "")
124
130
  .replace(/\/+$/, "");
125
131
 
126
- return normalizedGroupSlug
132
+ const href = normalizedGroupSlug
127
133
  ? `/${normalizedGroupSlug}/${endpointSlug}`
128
134
  : `/${endpointSlug}`;
135
+
136
+ return prependBasePath(href);
129
137
  }