radiant-docs 0.1.40 → 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.
@@ -0,0 +1,80 @@
1
+ import {
2
+ type AssistantChromeConfig,
3
+ getAssistantChromeConfig,
4
+ } from "./assistant-chrome";
5
+ import { withBasePath } from "./base-path";
6
+ import { getAssistantLauncherIconConfig } from "./assistant-embed-script";
7
+ import type { DocsConfig } from "./validation";
8
+
9
+ export type AssistantPanelRuntimeConfig = {
10
+ apiPath: string;
11
+ docsTitle: string;
12
+ isChatAvailable: boolean;
13
+ canSendChatRequest: boolean;
14
+ launcherThemeColor: string;
15
+ launcherThemeColors: {
16
+ light: string;
17
+ dark: string;
18
+ };
19
+ launcherIconColor: string;
20
+ launcherIconColors: {
21
+ light: string;
22
+ dark: string;
23
+ };
24
+ launcherIconImageSrc: string;
25
+ emptyStateHeading?: string;
26
+ emptyStateQuestions?: string[];
27
+ devProxyToken?: string;
28
+ chrome: AssistantChromeConfig;
29
+ };
30
+
31
+ export function getAssistantPanelRuntimeConfig(
32
+ config: DocsConfig,
33
+ ): AssistantPanelRuntimeConfig {
34
+ const assistantLauncherIcon = getAssistantLauncherIconConfig(config);
35
+ const assistantConfig = config.assistant;
36
+ const parsedOrgTier = Number.parseInt(
37
+ (import.meta.env.ORG_TIER ?? "1").toString(),
38
+ 10,
39
+ );
40
+ const orgTier =
41
+ Number.isFinite(parsedOrgTier) && parsedOrgTier > 0 ? parsedOrgTier : 1;
42
+ const isDev = import.meta.env.DEV;
43
+ const assistantDevHost = (import.meta.env.ASK_AI_DEV_HOST ?? "")
44
+ .toString()
45
+ .trim();
46
+ const assistantDevProxySecret = (
47
+ import.meta.env.ASK_AI_DEV_PROXY_SECRET ?? ""
48
+ )
49
+ .toString()
50
+ .trim();
51
+ const hasAssistantDevConfig =
52
+ assistantDevHost.length > 0 && assistantDevProxySecret.length > 0;
53
+ const isChatAvailable = isDev || orgTier >= 3;
54
+ const canSendChatRequest = isDev ? hasAssistantDevConfig : orgTier >= 3;
55
+ let apiPath = withBasePath("/_platform/assistant");
56
+
57
+ if (isDev && hasAssistantDevConfig) {
58
+ try {
59
+ apiPath = new URL("/_platform/assistant", assistantDevHost).toString();
60
+ } catch {
61
+ apiPath = withBasePath("/_platform/assistant");
62
+ }
63
+ }
64
+
65
+ return {
66
+ apiPath,
67
+ docsTitle: config.title,
68
+ isChatAvailable,
69
+ canSendChatRequest,
70
+ launcherThemeColor: assistantLauncherIcon.themeColor,
71
+ launcherThemeColors: assistantLauncherIcon.themeColors,
72
+ launcherIconColor: assistantLauncherIcon.color,
73
+ launcherIconColors: assistantLauncherIcon.colors,
74
+ launcherIconImageSrc: assistantLauncherIcon.imageSrc,
75
+ emptyStateHeading: assistantConfig?.heading,
76
+ emptyStateQuestions: assistantConfig?.questions,
77
+ devProxyToken: isDev ? assistantDevProxySecret : undefined,
78
+ chrome: getAssistantChromeConfig(config),
79
+ };
80
+ }
@@ -0,0 +1,31 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export type FaviconConfig = {
5
+ href: string;
6
+ type: string;
7
+ };
8
+
9
+ const DOCS_DIR = path.join(process.cwd(), "src/content/docs");
10
+ const DEFAULT_FAVICON: FaviconConfig = {
11
+ href: "/favicon.svg",
12
+ type: "image/svg+xml",
13
+ };
14
+
15
+ const FAVICON_CANDIDATES: FaviconConfig[] = [
16
+ DEFAULT_FAVICON,
17
+ { href: "/favicon.ico", type: "image/x-icon" },
18
+ { href: "/favicon.png", type: "image/png" },
19
+ { href: "/favicon.webp", type: "image/webp" },
20
+ { href: "/favicon.avif", type: "image/avif" },
21
+ { href: "/favicon.jpg", type: "image/jpeg" },
22
+ { href: "/favicon.jpeg", type: "image/jpeg" },
23
+ ];
24
+
25
+ export function getFaviconConfig(): FaviconConfig {
26
+ const favicon = FAVICON_CANDIDATES.find((candidate) =>
27
+ fs.existsSync(path.join(DOCS_DIR, candidate.href.replace(/^\/+/, ""))),
28
+ );
29
+
30
+ return favicon ?? DEFAULT_FAVICON;
31
+ }
@@ -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
+ }