radiant-docs 0.1.40 → 0.1.42

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 (43) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +42 -40
  3. package/template/package-lock.json +7 -0
  4. package/template/package.json +3 -2
  5. package/template/public/favicon.svg +16 -8
  6. package/template/scripts/remove-assistant-for-non-pro.mjs +28 -0
  7. package/template/src/components/Header.astro +151 -17
  8. package/template/src/components/MdxPage.astro +76 -22
  9. package/template/src/components/PagePagination.astro +44 -8
  10. package/template/src/components/Sidebar.astro +10 -1
  11. package/template/src/components/TableOfContents.astro +159 -53
  12. package/template/src/components/chat/AssistantDocsWidget.astro +16 -0
  13. package/template/src/components/chat/AssistantDocsWidget.tsx +615 -0
  14. package/template/src/components/chat/AssistantEmbedPanel.tsx +2679 -0
  15. package/template/src/components/chat/AssistantEmbedPanelPage.astro +95 -0
  16. package/template/src/components/user/Accordion.astro +2 -2
  17. package/template/src/components/user/AccordionGroup.astro +1 -1
  18. package/template/src/components/user/Callout.astro +10 -4
  19. package/template/src/components/user/Card.astro +488 -0
  20. package/template/src/components/user/CardGradient.astro +964 -0
  21. package/template/src/components/user/CodeBlock.astro +1 -1
  22. package/template/src/components/user/CodeGroup.astro +1 -1
  23. package/template/src/components/user/Column.astro +25 -0
  24. package/template/src/components/user/Columns.astro +200 -0
  25. package/template/src/components/user/ComponentPreviewBlock.astro +1 -1
  26. package/template/src/components/user/Image.astro +1 -1
  27. package/template/src/components/user/Step.astro +1 -1
  28. package/template/src/components/user/Steps.astro +1 -1
  29. package/template/src/components/user/Tab.astro +1 -3
  30. package/template/src/components/user/Tabs.astro +2 -2
  31. package/template/src/layouts/Layout.astro +13 -156
  32. package/template/src/lib/assistant-chrome-defaults.ts +86 -0
  33. package/template/src/lib/assistant-chrome.ts +39 -0
  34. package/template/src/lib/assistant-embed-script.ts +1088 -0
  35. package/template/src/lib/assistant-panel-config.ts +80 -0
  36. package/template/src/lib/favicon.ts +31 -0
  37. package/template/src/lib/theme-css.ts +176 -0
  38. package/template/src/lib/validation.ts +668 -41
  39. package/template/src/pages/-/assistant/embed.js.ts +15 -0
  40. package/template/src/pages/-/assistant/panel.astro +5 -0
  41. package/template/src/pages/404.astro +4 -4
  42. package/template/src/styles/global.css +81 -4
  43. package/template/src/components/chat/AskAiWidget.tsx +0 -2011
@@ -110,6 +110,9 @@ const AVAILABLE_COMPONENTS = [
110
110
  "Step",
111
111
  "Accordion",
112
112
  "AccordionGroup",
113
+ "Card",
114
+ "Column",
115
+ "Columns",
113
116
  "Image",
114
117
  "CodeGroup",
115
118
  "ComponentPreview",
@@ -166,6 +169,7 @@ export type NavbarItem = {
166
169
  text: string;
167
170
  href: string;
168
171
  icon?: string | null;
172
+ color?: string | ThemeColorByMode;
169
173
  };
170
174
  export type HiddenPageRoute = {
171
175
  filePath: string;
@@ -208,17 +212,51 @@ export type BaseColorByMode = {
208
212
  export const DEFAULT_THEME_COLOR_LIGHT = "#171717";
209
213
  export const DEFAULT_THEME_COLOR_DARK = "#f5f5f5";
210
214
  export type ThemeColorByMode = {
211
- light: string;
212
- dark: string;
215
+ light?: string;
216
+ dark?: string;
217
+ };
218
+ export type CardCoverTheme = {
219
+ colors?: string[];
220
+ colorSeed?: string;
221
+ };
222
+ export type CardButtonTheme = {
223
+ color?: string | ThemeColorByMode;
224
+ };
225
+ export type CardTheme = {
226
+ cover?: CardCoverTheme;
227
+ button?: CardButtonTheme;
213
228
  };
214
229
  export type DocsTheme = {
215
230
  baseColor?: BaseColorOption | BaseColorByMode;
216
231
  themeColor?: string | ThemeColorByMode;
232
+ card?: CardTheme;
233
+ };
234
+ export type AssistantIcon = {
235
+ src?: string;
236
+ color?: string;
237
+ };
238
+ export type AssistantButtonSize = "small" | "default";
239
+ export type AssistantButtonConfig = {
240
+ size?: AssistantButtonSize;
241
+ color?: string | ThemeColorByMode;
242
+ };
243
+ export type AssistantNavbarButtonConfig = {
244
+ enabled?: boolean;
245
+ text?: string;
246
+ color?: string | ThemeColorByMode;
247
+ };
248
+ export type AssistantConfig = {
249
+ button?: AssistantButtonConfig;
250
+ navbarButton?: AssistantNavbarButtonConfig;
251
+ heading?: string;
252
+ questions?: string[];
253
+ icon?: AssistantIcon;
217
254
  };
218
255
  export type DocsConfig = {
219
256
  title: string;
220
257
  logo?: Logo;
221
258
  theme?: DocsTheme;
259
+ assistant?: AssistantConfig;
222
260
  home?: string;
223
261
  navigation: NavigationItem;
224
262
  navbar?: {
@@ -292,6 +330,125 @@ function checkType(
292
330
  }
293
331
  }
294
332
 
333
+ function normalizeHexColor(
334
+ value: unknown,
335
+ currentPath: Path,
336
+ label: string,
337
+ ): string {
338
+ checkType(value, "string", currentPath, label);
339
+ if (typeof value !== "string") {
340
+ throwConfigError(`${label} must be a string.`, currentPath);
341
+ }
342
+
343
+ const trimmedValue = value.trim();
344
+ if (trimmedValue.length === 0) {
345
+ throwConfigError(`${label} cannot be empty.`, currentPath);
346
+ }
347
+
348
+ const normalizedValue = trimmedValue.startsWith("#")
349
+ ? trimmedValue
350
+ : `#${trimmedValue}`;
351
+ if (
352
+ !/^#(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(
353
+ normalizedValue,
354
+ )
355
+ ) {
356
+ throwConfigError(
357
+ `${label} must be a valid hex color (for example: #1d4ed8).`,
358
+ currentPath,
359
+ );
360
+ }
361
+
362
+ return normalizedValue.toLowerCase();
363
+ }
364
+
365
+ function normalizeThemeColorConfig(
366
+ value: unknown,
367
+ currentPath: Path,
368
+ label: string,
369
+ ): string | ThemeColorByMode {
370
+ if (typeof value === "string") {
371
+ return normalizeHexColor(value, currentPath, label);
372
+ }
373
+
374
+ checkType(value, "object", currentPath, label);
375
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
376
+ throwConfigError(
377
+ `${label} must be a string or an object with light/dark values.`,
378
+ currentPath,
379
+ );
380
+ }
381
+
382
+ const colorByMode = value as Record<string, unknown>;
383
+ const allowedKeys = new Set(["light", "dark"]);
384
+ for (const key of Object.keys(colorByMode)) {
385
+ if (!allowedKeys.has(key)) {
386
+ throwConfigError(`${label} object only supports 'light' and 'dark'.`, [
387
+ ...currentPath,
388
+ key,
389
+ ]);
390
+ }
391
+ }
392
+
393
+ const light =
394
+ colorByMode.light !== undefined
395
+ ? normalizeHexColor(colorByMode.light, [...currentPath, "light"], label)
396
+ : undefined;
397
+ const dark =
398
+ colorByMode.dark !== undefined
399
+ ? normalizeHexColor(colorByMode.dark, [...currentPath, "dark"], label)
400
+ : undefined;
401
+
402
+ if (light === undefined && dark === undefined) {
403
+ throwConfigError(
404
+ `${label} object must include 'light', 'dark', or both.`,
405
+ currentPath,
406
+ );
407
+ }
408
+
409
+ return {
410
+ ...(light !== undefined ? { light } : {}),
411
+ ...(dark !== undefined ? { dark } : {}),
412
+ };
413
+ }
414
+
415
+ function normalizeHexColorArray(
416
+ value: unknown,
417
+ currentPath: Path,
418
+ label: string,
419
+ ): string[] {
420
+ checkType(value, "array", currentPath, label);
421
+ if (!Array.isArray(value)) {
422
+ throwConfigError(`${label} must be an array.`, currentPath);
423
+ }
424
+
425
+ if (value.length < 1 || value.length > 4) {
426
+ throwConfigError(`${label} must include 1 to 4 colors.`, currentPath);
427
+ }
428
+
429
+ return value.map((color, index) =>
430
+ normalizeHexColor(color, [...currentPath, index], `${label} ${index + 1}`),
431
+ );
432
+ }
433
+
434
+ function normalizeSeedValue(
435
+ value: unknown,
436
+ currentPath: Path,
437
+ label: string,
438
+ ): string {
439
+ checkType(value, "string", currentPath, label);
440
+ if (typeof value !== "string") {
441
+ throwConfigError(`${label} must be a string.`, currentPath);
442
+ }
443
+
444
+ const trimmedValue = value.trim();
445
+ if (trimmedValue.length === 0) {
446
+ throwConfigError(`${label} cannot be empty.`, currentPath);
447
+ }
448
+
449
+ return trimmedValue;
450
+ }
451
+
295
452
  function validateFileExistence(filePath: string, currentPath: Path): void {
296
453
  // Assuming relative path from DOCS_DIR and .mdx extension
297
454
  const fullPath = path.join(DOCS_DIR, `${filePath}.mdx`);
@@ -370,7 +527,11 @@ function normalizeInternalPageHref(
370
527
  return null;
371
528
  }
372
529
 
373
- const filePath = normalizeDocsPagePath(normalizedPathname, currentPath, label);
530
+ const filePath = normalizeDocsPagePath(
531
+ normalizedPathname,
532
+ currentPath,
533
+ label,
534
+ );
374
535
  validateFileExistence(filePath, currentPath);
375
536
 
376
537
  return {
@@ -1172,6 +1333,21 @@ function validateNavbarItem(
1172
1333
 
1173
1334
  // Optional property
1174
1335
  validateIcon(item.icon, [...currentPath, "icon"]);
1336
+ if (item.color !== undefined) {
1337
+ if (currentPath[0] !== "navbar" || currentPath[1] !== "primary") {
1338
+ throwConfigError(
1339
+ "Navbar item color is only supported on navbar.primary.",
1340
+ [...currentPath, "color"],
1341
+ );
1342
+ }
1343
+
1344
+ item.color = normalizeThemeColorConfig(
1345
+ item.color,
1346
+ [...currentPath, "color"],
1347
+ "Navbar primary color",
1348
+ );
1349
+ }
1350
+
1175
1351
  return hiddenPageRoute;
1176
1352
  }
1177
1353
 
@@ -1222,10 +1398,85 @@ function validateLogoImagePath(
1222
1398
 
1223
1399
  if (!fs.existsSync(fullPath)) {
1224
1400
  throwConfigError(
1225
- `${label} file not found. Expected: ${normalizedPath} (relative to content/docs folder)`,
1401
+ `${label} file not found. Expected: ${normalizedPath}`,
1402
+ currentPath,
1403
+ );
1404
+ }
1405
+ }
1406
+
1407
+ function validateAssistantIconSource(
1408
+ iconSource: unknown,
1409
+ currentPath: Path,
1410
+ ): string {
1411
+ checkType(iconSource, "string", currentPath, "Assistant icon source");
1412
+ if (typeof iconSource !== "string") {
1413
+ throwConfigError("Assistant icon source must be a string.", currentPath);
1414
+ }
1415
+
1416
+ const trimmedSource = iconSource.trim();
1417
+ if (trimmedSource.length === 0) {
1418
+ throwConfigError("Assistant icon source cannot be empty.", currentPath);
1419
+ }
1420
+
1421
+ if (isUrl(trimmedSource)) {
1422
+ throwConfigError(
1423
+ "Assistant icon source must be a local image path relative to docs.json.",
1424
+ currentPath,
1425
+ );
1426
+ }
1427
+
1428
+ if (trimmedSource.includes(":")) {
1429
+ throwConfigError(
1430
+ `Invalid assistant icon source: "${trimmedSource}". Assistant icons must be local image files, not Iconify icon names.`,
1431
+ currentPath,
1432
+ );
1433
+ }
1434
+
1435
+ if (
1436
+ trimmedSource.startsWith("//") ||
1437
+ trimmedSource.startsWith("#") ||
1438
+ trimmedSource.startsWith("?") ||
1439
+ trimmedSource.startsWith("./") ||
1440
+ trimmedSource.startsWith("../")
1441
+ ) {
1442
+ throwConfigError(
1443
+ "Assistant icon source must be a local image path relative to docs.json.",
1444
+ currentPath,
1445
+ );
1446
+ }
1447
+
1448
+ const parsed = new URL(trimmedSource, "https://docs.invalid/");
1449
+ const validExtensions = [
1450
+ ".svg",
1451
+ ".png",
1452
+ ".jpg",
1453
+ ".jpeg",
1454
+ ".webp",
1455
+ ".gif",
1456
+ ".ico",
1457
+ ".avif",
1458
+ ];
1459
+ const hasValidExtension = validExtensions.some((ext) =>
1460
+ parsed.pathname.toLowerCase().endsWith(ext),
1461
+ );
1462
+ if (!hasValidExtension) {
1463
+ throwConfigError(
1464
+ "Assistant icon source must be a valid image file (.svg, .png, .jpg, .jpeg, .webp, .gif, .ico, .avif).",
1465
+ currentPath,
1466
+ );
1467
+ }
1468
+
1469
+ const normalizedPath = parsed.pathname.replace(/^\/+/, "");
1470
+ const fullPath = path.join(DOCS_DIR, normalizedPath);
1471
+
1472
+ if (!fs.existsSync(fullPath)) {
1473
+ throwConfigError(
1474
+ `Assistant icon source file not found. Expected: ${normalizedPath}`,
1226
1475
  currentPath,
1227
1476
  );
1228
1477
  }
1478
+
1479
+ return trimmedSource;
1229
1480
  }
1230
1481
 
1231
1482
  function validateLogoVariant(
@@ -1397,38 +1648,6 @@ function validateTheme(theme: DocsConfig["theme"]): void {
1397
1648
  return normalizedBaseColor as BaseColorOption;
1398
1649
  };
1399
1650
 
1400
- const normalizeThemeColor = (
1401
- value: unknown,
1402
- currentPath: Path,
1403
- label: string,
1404
- ): string => {
1405
- checkType(value, "string", currentPath, label);
1406
- if (typeof value !== "string") {
1407
- throwConfigError(`${label} must be a string.`, currentPath);
1408
- }
1409
-
1410
- const trimmedValue = (value as string).trim();
1411
- if (trimmedValue.length === 0) {
1412
- throwConfigError(`${label} cannot be empty.`, currentPath);
1413
- }
1414
-
1415
- const normalizedValue = trimmedValue.startsWith("#")
1416
- ? trimmedValue
1417
- : `#${trimmedValue}`;
1418
- if (
1419
- !/^#(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(
1420
- normalizedValue,
1421
- )
1422
- ) {
1423
- throwConfigError(
1424
- `${label} must be a valid hex color (for example: #1d4ed8).`,
1425
- currentPath,
1426
- );
1427
- }
1428
-
1429
- return normalizedValue.toLowerCase();
1430
- };
1431
-
1432
1651
  if (theme.baseColor !== undefined) {
1433
1652
  if (typeof theme.baseColor === "string") {
1434
1653
  theme.baseColor = normalizeBaseColor(
@@ -1499,12 +1718,122 @@ function validateTheme(theme: DocsConfig["theme"]): void {
1499
1718
  }
1500
1719
  }
1501
1720
 
1721
+ if (theme.card !== undefined) {
1722
+ checkType(theme.card, "object", ["theme", "card"], "Theme card");
1723
+ if (
1724
+ typeof theme.card !== "object" ||
1725
+ theme.card === null ||
1726
+ Array.isArray(theme.card)
1727
+ ) {
1728
+ throwConfigError("Theme card must be an object.", ["theme", "card"]);
1729
+ }
1730
+
1731
+ const cardTheme = theme.card as CardTheme & Record<string, unknown>;
1732
+ const allowedCardKeys = new Set(["cover", "button"]);
1733
+ for (const key of Object.keys(cardTheme)) {
1734
+ if (!allowedCardKeys.has(key)) {
1735
+ throwConfigError(
1736
+ "Theme card configuration only supports 'cover' and 'button'.",
1737
+ ["theme", "card", key],
1738
+ );
1739
+ }
1740
+ }
1741
+
1742
+ if (cardTheme.cover !== undefined) {
1743
+ checkType(
1744
+ cardTheme.cover,
1745
+ "object",
1746
+ ["theme", "card", "cover"],
1747
+ "Theme card cover",
1748
+ );
1749
+ if (
1750
+ typeof cardTheme.cover !== "object" ||
1751
+ cardTheme.cover === null ||
1752
+ Array.isArray(cardTheme.cover)
1753
+ ) {
1754
+ throwConfigError("Theme card cover must be an object.", [
1755
+ "theme",
1756
+ "card",
1757
+ "cover",
1758
+ ]);
1759
+ }
1760
+
1761
+ const coverTheme = cardTheme.cover as CardCoverTheme &
1762
+ Record<string, unknown>;
1763
+ const allowedCoverKeys = new Set(["colors", "colorSeed"]);
1764
+ for (const key of Object.keys(coverTheme)) {
1765
+ if (!allowedCoverKeys.has(key)) {
1766
+ throwConfigError(
1767
+ "Theme card cover configuration only supports 'colors' and 'colorSeed'.",
1768
+ ["theme", "card", "cover", key],
1769
+ );
1770
+ }
1771
+ }
1772
+
1773
+ if (coverTheme.colors !== undefined) {
1774
+ coverTheme.colors = normalizeHexColorArray(
1775
+ coverTheme.colors,
1776
+ ["theme", "card", "cover", "colors"],
1777
+ "Theme card cover colors",
1778
+ );
1779
+ }
1780
+
1781
+ if (coverTheme.colorSeed !== undefined) {
1782
+ coverTheme.colorSeed = normalizeSeedValue(
1783
+ coverTheme.colorSeed,
1784
+ ["theme", "card", "cover", "colorSeed"],
1785
+ "Theme card cover color seed",
1786
+ );
1787
+ }
1788
+ }
1789
+
1790
+ if (cardTheme.button !== undefined) {
1791
+ checkType(
1792
+ cardTheme.button,
1793
+ "object",
1794
+ ["theme", "card", "button"],
1795
+ "Theme card button",
1796
+ );
1797
+ if (
1798
+ typeof cardTheme.button !== "object" ||
1799
+ cardTheme.button === null ||
1800
+ Array.isArray(cardTheme.button)
1801
+ ) {
1802
+ throwConfigError("Theme card button must be an object.", [
1803
+ "theme",
1804
+ "card",
1805
+ "button",
1806
+ ]);
1807
+ }
1808
+
1809
+ const buttonTheme = cardTheme.button as CardButtonTheme &
1810
+ Record<string, unknown>;
1811
+ const allowedButtonKeys = new Set(["color"]);
1812
+ for (const key of Object.keys(buttonTheme)) {
1813
+ if (!allowedButtonKeys.has(key)) {
1814
+ throwConfigError(
1815
+ "Theme card button configuration only supports 'color'.",
1816
+ ["theme", "card", "button", key],
1817
+ );
1818
+ }
1819
+ }
1820
+
1821
+ if (buttonTheme.color !== undefined) {
1822
+ buttonTheme.color = normalizeThemeColorConfig(
1823
+ buttonTheme.color,
1824
+ ["theme", "card", "button", "color"],
1825
+ "Theme card button color",
1826
+ );
1827
+ }
1828
+ }
1829
+ }
1830
+
1502
1831
  if (theme.themeColor === undefined) {
1503
1832
  return;
1504
1833
  }
1505
1834
 
1506
1835
  if (typeof theme.themeColor === "string") {
1507
- theme.themeColor = normalizeThemeColor(
1836
+ theme.themeColor = normalizeHexColor(
1508
1837
  theme.themeColor,
1509
1838
  ["theme", "themeColor"],
1510
1839
  "Theme color",
@@ -1538,7 +1867,7 @@ function validateTheme(theme: DocsConfig["theme"]): void {
1538
1867
 
1539
1868
  const light =
1540
1869
  themeColorByMode.light !== undefined
1541
- ? normalizeThemeColor(
1870
+ ? normalizeHexColor(
1542
1871
  themeColorByMode.light,
1543
1872
  ["theme", "themeColor", "light"],
1544
1873
  "Theme color light",
@@ -1546,7 +1875,7 @@ function validateTheme(theme: DocsConfig["theme"]): void {
1546
1875
  : undefined;
1547
1876
  const dark =
1548
1877
  themeColorByMode.dark !== undefined
1549
- ? normalizeThemeColor(
1878
+ ? normalizeHexColor(
1550
1879
  themeColorByMode.dark,
1551
1880
  ["theme", "themeColor", "dark"],
1552
1881
  "Theme color dark",
@@ -1561,11 +1890,308 @@ function validateTheme(theme: DocsConfig["theme"]): void {
1561
1890
  }
1562
1891
 
1563
1892
  theme.themeColor = {
1564
- light: light ?? DEFAULT_THEME_COLOR_LIGHT,
1565
- dark: dark ?? DEFAULT_THEME_COLOR_DARK,
1893
+ ...(light !== undefined ? { light } : {}),
1894
+ ...(dark !== undefined ? { dark } : {}),
1566
1895
  };
1567
1896
  }
1568
1897
 
1898
+ function validateAssistant(assistant: DocsConfig["assistant"]): void {
1899
+ if (assistant === undefined) return;
1900
+
1901
+ checkType(assistant, "object", ["assistant"], "Assistant configuration");
1902
+ if (
1903
+ typeof assistant !== "object" ||
1904
+ assistant === null ||
1905
+ Array.isArray(assistant)
1906
+ ) {
1907
+ throwConfigError("Assistant configuration must be an object.", [
1908
+ "assistant",
1909
+ ]);
1910
+ }
1911
+
1912
+ const allowedAssistantKeys = new Set([
1913
+ "button",
1914
+ "navbarButton",
1915
+ "heading",
1916
+ "questions",
1917
+ "icon",
1918
+ ]);
1919
+ for (const key of Object.keys(assistant)) {
1920
+ if (!allowedAssistantKeys.has(key)) {
1921
+ throwConfigError(
1922
+ "Assistant configuration only supports 'button', 'navbarButton', 'heading', 'questions', and 'icon'.",
1923
+ ["assistant", key],
1924
+ );
1925
+ }
1926
+ }
1927
+
1928
+ if (assistant.button !== undefined) {
1929
+ checkType(
1930
+ assistant.button,
1931
+ "object",
1932
+ ["assistant", "button"],
1933
+ "Assistant button configuration",
1934
+ );
1935
+ if (
1936
+ typeof assistant.button !== "object" ||
1937
+ assistant.button === null ||
1938
+ Array.isArray(assistant.button)
1939
+ ) {
1940
+ throwConfigError("Assistant button configuration must be an object.", [
1941
+ "assistant",
1942
+ "button",
1943
+ ]);
1944
+ }
1945
+
1946
+ const allowedButtonKeys = new Set(["size", "color"]);
1947
+ for (const key of Object.keys(assistant.button)) {
1948
+ if (!allowedButtonKeys.has(key)) {
1949
+ throwConfigError(
1950
+ "Assistant button configuration only supports 'size' and 'color'.",
1951
+ ["assistant", "button", key],
1952
+ );
1953
+ }
1954
+ }
1955
+
1956
+ if (assistant.button.size !== undefined) {
1957
+ checkType(
1958
+ assistant.button.size,
1959
+ "string",
1960
+ ["assistant", "button", "size"],
1961
+ "Assistant button size",
1962
+ );
1963
+ if (typeof assistant.button.size !== "string") {
1964
+ throwConfigError("Assistant button size must be a string.", [
1965
+ "assistant",
1966
+ "button",
1967
+ "size",
1968
+ ]);
1969
+ }
1970
+
1971
+ const trimmedSize = assistant.button.size.trim();
1972
+ if (trimmedSize !== "small" && trimmedSize !== "default") {
1973
+ throwConfigError(
1974
+ "Assistant button size must be either 'small' or 'default'.",
1975
+ ["assistant", "button", "size"],
1976
+ );
1977
+ }
1978
+ assistant.button.size = trimmedSize;
1979
+ }
1980
+
1981
+ if (assistant.button.color !== undefined) {
1982
+ assistant.button.color = normalizeThemeColorConfig(
1983
+ assistant.button.color,
1984
+ ["assistant", "button", "color"],
1985
+ "Assistant button color",
1986
+ );
1987
+ }
1988
+ }
1989
+
1990
+ if (assistant.navbarButton !== undefined) {
1991
+ checkType(
1992
+ assistant.navbarButton,
1993
+ "object",
1994
+ ["assistant", "navbarButton"],
1995
+ "Assistant navbar button configuration",
1996
+ );
1997
+ if (
1998
+ typeof assistant.navbarButton !== "object" ||
1999
+ assistant.navbarButton === null ||
2000
+ Array.isArray(assistant.navbarButton)
2001
+ ) {
2002
+ throwConfigError(
2003
+ "Assistant navbar button configuration must be an object.",
2004
+ ["assistant", "navbarButton"],
2005
+ );
2006
+ }
2007
+
2008
+ const allowedNavbarButtonKeys = new Set(["enabled", "text", "color"]);
2009
+ for (const key of Object.keys(assistant.navbarButton)) {
2010
+ if (!allowedNavbarButtonKeys.has(key)) {
2011
+ throwConfigError(
2012
+ "Assistant navbar button configuration only supports 'enabled', 'text', and 'color'.",
2013
+ ["assistant", "navbarButton", key],
2014
+ );
2015
+ }
2016
+ }
2017
+
2018
+ if (assistant.navbarButton.enabled !== undefined) {
2019
+ checkType(
2020
+ assistant.navbarButton.enabled,
2021
+ "boolean",
2022
+ ["assistant", "navbarButton", "enabled"],
2023
+ "Assistant navbar button enabled",
2024
+ );
2025
+ if (typeof assistant.navbarButton.enabled !== "boolean") {
2026
+ throwConfigError(
2027
+ "Assistant navbar button enabled must be a boolean.",
2028
+ ["assistant", "navbarButton", "enabled"],
2029
+ );
2030
+ }
2031
+ }
2032
+
2033
+ if (assistant.navbarButton.text !== undefined) {
2034
+ checkType(
2035
+ assistant.navbarButton.text,
2036
+ "string",
2037
+ ["assistant", "navbarButton", "text"],
2038
+ "Assistant navbar button text",
2039
+ );
2040
+ if (typeof assistant.navbarButton.text !== "string") {
2041
+ throwConfigError("Assistant navbar button text must be a string.", [
2042
+ "assistant",
2043
+ "navbarButton",
2044
+ "text",
2045
+ ]);
2046
+ }
2047
+
2048
+ const trimmedText = assistant.navbarButton.text.trim();
2049
+ if (trimmedText.length === 0) {
2050
+ throwConfigError("Assistant navbar button text cannot be empty.", [
2051
+ "assistant",
2052
+ "navbarButton",
2053
+ "text",
2054
+ ]);
2055
+ }
2056
+ assistant.navbarButton.text = trimmedText;
2057
+ }
2058
+
2059
+ if (assistant.navbarButton.color !== undefined) {
2060
+ assistant.navbarButton.color = normalizeThemeColorConfig(
2061
+ assistant.navbarButton.color,
2062
+ ["assistant", "navbarButton", "color"],
2063
+ "Assistant navbar button color",
2064
+ );
2065
+ }
2066
+ }
2067
+
2068
+ if (assistant.heading !== undefined) {
2069
+ checkType(
2070
+ assistant.heading,
2071
+ "string",
2072
+ ["assistant", "heading"],
2073
+ "Assistant heading",
2074
+ );
2075
+ if (typeof assistant.heading !== "string") {
2076
+ throwConfigError("Assistant heading must be a string.", [
2077
+ "assistant",
2078
+ "heading",
2079
+ ]);
2080
+ }
2081
+
2082
+ const trimmedHeading = assistant.heading.trim();
2083
+ if (trimmedHeading.length === 0) {
2084
+ throwConfigError("Assistant heading cannot be empty.", [
2085
+ "assistant",
2086
+ "heading",
2087
+ ]);
2088
+ }
2089
+ assistant.heading = trimmedHeading;
2090
+ }
2091
+
2092
+ if (assistant.questions !== undefined) {
2093
+ checkType(
2094
+ assistant.questions,
2095
+ "array",
2096
+ ["assistant", "questions"],
2097
+ "Assistant questions",
2098
+ );
2099
+ if (!Array.isArray(assistant.questions)) {
2100
+ throwConfigError("Assistant questions must be an array.", [
2101
+ "assistant",
2102
+ "questions",
2103
+ ]);
2104
+ }
2105
+
2106
+ if (assistant.questions.length > 3) {
2107
+ throwConfigError("Assistant questions can include at most 3 questions.", [
2108
+ "assistant",
2109
+ "questions",
2110
+ ]);
2111
+ }
2112
+
2113
+ assistant.questions = assistant.questions.map((question, index) => {
2114
+ checkType(
2115
+ question,
2116
+ "string",
2117
+ ["assistant", "questions", String(index)],
2118
+ "Assistant question",
2119
+ );
2120
+ if (typeof question !== "string") {
2121
+ throwConfigError("Assistant question must be a string.", [
2122
+ "assistant",
2123
+ "questions",
2124
+ String(index),
2125
+ ]);
2126
+ }
2127
+
2128
+ const trimmedQuestion = question.trim();
2129
+ if (trimmedQuestion.length === 0) {
2130
+ throwConfigError(
2131
+ "Assistant question cannot be empty.",
2132
+ ["assistant", "questions", String(index)],
2133
+ );
2134
+ }
2135
+
2136
+ return trimmedQuestion;
2137
+ });
2138
+ }
2139
+
2140
+ if (assistant.icon === undefined) return;
2141
+
2142
+ checkType(
2143
+ assistant.icon,
2144
+ "object",
2145
+ ["assistant", "icon"],
2146
+ "Assistant icon",
2147
+ );
2148
+ if (
2149
+ typeof assistant.icon !== "object" ||
2150
+ assistant.icon === null ||
2151
+ Array.isArray(assistant.icon)
2152
+ ) {
2153
+ throwConfigError("Assistant icon must be an object.", [
2154
+ "assistant",
2155
+ "icon",
2156
+ ]);
2157
+ }
2158
+
2159
+ const allowedIconKeys = new Set(["src", "color"]);
2160
+ for (const key of Object.keys(assistant.icon)) {
2161
+ if (!allowedIconKeys.has(key)) {
2162
+ throwConfigError(
2163
+ "Assistant icon only supports 'src' and 'color'.",
2164
+ ["assistant", "icon", key],
2165
+ );
2166
+ }
2167
+ }
2168
+
2169
+ if (
2170
+ assistant.icon.src === undefined &&
2171
+ assistant.icon.color === undefined
2172
+ ) {
2173
+ throwConfigError(
2174
+ "Assistant icon must include 'src', 'color', or both.",
2175
+ ["assistant", "icon"],
2176
+ );
2177
+ }
2178
+
2179
+ if (assistant.icon.src !== undefined) {
2180
+ assistant.icon.src = validateAssistantIconSource(
2181
+ assistant.icon.src,
2182
+ ["assistant", "icon", "src"],
2183
+ );
2184
+ }
2185
+
2186
+ if (assistant.icon.color !== undefined) {
2187
+ assistant.icon.color = normalizeHexColor(
2188
+ assistant.icon.color,
2189
+ ["assistant", "icon", "color"],
2190
+ "Assistant icon color",
2191
+ );
2192
+ }
2193
+ }
2194
+
1569
2195
  function validateHome(home: DocsConfig["home"]): string | undefined {
1570
2196
  if (home === undefined) return undefined;
1571
2197
 
@@ -1765,6 +2391,7 @@ async function validateConfig(config: any): Promise<DocsConfig> {
1765
2391
  validateTitle(config.title);
1766
2392
  validateLogo(config.logo);
1767
2393
  validateTheme(config.theme);
2394
+ validateAssistant(config.assistant);
1768
2395
  await validateNavigation(config.navigation);
1769
2396
  config.home = validateHome(config.home);
1770
2397