radiant-docs 0.1.49 → 0.1.51

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/dist/index.js CHANGED
@@ -36,6 +36,7 @@ var log = {
36
36
  ),
37
37
  blank: () => console.log("")
38
38
  };
39
+ var SUPPORTED_COMMANDS = /* @__PURE__ */ new Set(["dev", "check"]);
39
40
  function getUserDocsDir() {
40
41
  return process.cwd();
41
42
  }
@@ -75,6 +76,9 @@ async function getDirectoryFingerprint(dir) {
75
76
  await walk(dir);
76
77
  return hash.digest("hex");
77
78
  }
79
+ function hasFrameworkDependencies(cacheDir) {
80
+ return fs.existsSync(path.join(cacheDir, "node_modules/.bin/astro"));
81
+ }
78
82
  async function validateUserDocs(docsDir) {
79
83
  const docsJsonPath = path.join(docsDir, "docs.json");
80
84
  if (!fs.existsSync(docsJsonPath)) {
@@ -94,7 +98,7 @@ async function validateUserDocs(docsDir) {
94
98
  }
95
99
  async function setupCache(cacheDir) {
96
100
  const templateDir = getTemplateDir();
97
- const nodeModulesExists = fs.existsSync(path.join(cacheDir, "node_modules"));
101
+ const dependenciesInstalled = hasFrameworkDependencies(cacheDir);
98
102
  const cacheExists = fs.existsSync(cacheDir);
99
103
  if (!fs.existsSync(templateDir)) {
100
104
  log.error(
@@ -132,7 +136,7 @@ async function setupCache(cacheDir) {
132
136
  log.success("Framework copied to cache.");
133
137
  return true;
134
138
  }
135
- return !nodeModulesExists;
139
+ return !dependenciesInstalled;
136
140
  }
137
141
  async function installDependencies(cacheDir) {
138
142
  log.info("Installing dependencies (first run only)...");
@@ -149,6 +153,7 @@ async function installDependencies(cacheDir) {
149
153
  });
150
154
  log.success("Dependencies installed.");
151
155
  } catch (error) {
156
+ await fs.remove(path.join(cacheDir, "node_modules"));
152
157
  log.error("Failed to install dependencies.");
153
158
  log.error(`Error details: ${error.message}`);
154
159
  throw error;
@@ -179,6 +184,66 @@ async function runAstroSync(cacheDir) {
179
184
  } catch {
180
185
  }
181
186
  }
187
+ function toCommandOutput(value) {
188
+ if (!value) return "";
189
+ if (Buffer.isBuffer(value)) return value.toString("utf-8");
190
+ return String(value);
191
+ }
192
+ function getCommandOutput(error) {
193
+ return [
194
+ toCommandOutput(error?.stdout),
195
+ toCommandOutput(error?.stderr),
196
+ toCommandOutput(error?.message)
197
+ ].filter(Boolean).join("\n");
198
+ }
199
+ function extractUserErrors(output) {
200
+ const errors = [];
201
+ for (const line of output.split("\n")) {
202
+ if (!line.includes("[USER_ERROR]")) continue;
203
+ const message = line.split("[USER_ERROR]:")[1]?.trim() || line.trim();
204
+ if (message && !errors.includes(message)) {
205
+ errors.push(message);
206
+ }
207
+ }
208
+ return errors;
209
+ }
210
+ function getOutputTail(output, maxLines = 40) {
211
+ return output.split("\n").map((line) => line.trim()).filter(Boolean).slice(-maxLines).join("\n");
212
+ }
213
+ function runFrameworkCheck(cacheDir) {
214
+ log.info("Checking documentation...");
215
+ try {
216
+ execSync("npm run check", {
217
+ cwd: cacheDir,
218
+ stdio: "pipe",
219
+ env: {
220
+ ...process.env,
221
+ RADIANT_CHECK: "1"
222
+ },
223
+ encoding: "utf-8"
224
+ });
225
+ log.success("Documentation check passed.");
226
+ } catch (error) {
227
+ const output = getCommandOutput(error);
228
+ const userErrors = extractUserErrors(output);
229
+ log.blank();
230
+ log.error("Documentation check failed.");
231
+ if (userErrors.length > 0) {
232
+ log.blank();
233
+ for (const userError of userErrors) {
234
+ log.error(userError);
235
+ }
236
+ } else {
237
+ const outputTail = getOutputTail(output);
238
+ if (outputTail) {
239
+ log.blank();
240
+ console.log(outputTail);
241
+ }
242
+ }
243
+ log.blank();
244
+ process.exit(typeof error?.status === "number" ? error.status : 1);
245
+ }
246
+ }
182
247
  function startDevServer(cacheDir) {
183
248
  const child = spawn("npm", ["run", "dev"], {
184
249
  cwd: cacheDir,
@@ -284,12 +349,13 @@ ${colors.bright}${colors.cyan}\u25C6 Radiant${colors.reset} v${VERSION}
284
349
  ${colors.bright}Usage:${colors.reset}
285
350
  radiant Start the dev server
286
351
  radiant dev Start the dev server
352
+ radiant check Validate and build the docs once
287
353
  radiant --help Show this help message
288
354
  radiant --version Show version
289
355
 
290
356
  ${colors.bright}Description:${colors.reset}
291
357
  Run this command from your documentation folder (containing docs.json)
292
- to preview your docs locally with hot reload.
358
+ to preview your docs locally with hot reload, or run a one-time check.
293
359
  `);
294
360
  }
295
361
  async function run() {
@@ -302,8 +368,8 @@ async function run() {
302
368
  console.log(VERSION);
303
369
  process.exit(0);
304
370
  }
305
- const command = args[0];
306
- if (command && command !== "dev") {
371
+ const command = args[0] || "dev";
372
+ if (!SUPPORTED_COMMANDS.has(command)) {
307
373
  log.error(`Unknown command: ${command}`);
308
374
  showHelp();
309
375
  process.exit(1);
@@ -325,6 +391,10 @@ async function run() {
325
391
  await syncContent(docsDir, contentDir);
326
392
  await runAstroSync(cacheDir);
327
393
  log.success("Content synced.");
394
+ if (command === "check") {
395
+ runFrameworkCheck(cacheDir);
396
+ return;
397
+ }
328
398
  log.info("Starting dev server...");
329
399
  const devServer = startDevServer(cacheDir);
330
400
  const watcher = watchUserContent(docsDir, contentDir, async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "radiant-docs",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "description": "CLI tool for previewing Radiant documentation locally",
5
5
  "type": "module",
6
6
  "bin": {
@@ -68,8 +68,13 @@ const headingAnchorContent = {
68
68
  ],
69
69
  };
70
70
 
71
- if (process.env.npm_lifecycle_event === "build") {
72
- // Run validation for build before Astro's internal validation
71
+ const shouldValidateDocs =
72
+ process.env.npm_lifecycle_event === "build" ||
73
+ process.env.npm_lifecycle_event === "check" ||
74
+ process.env.RADIANT_CHECK === "1";
75
+
76
+ if (shouldValidateDocs) {
77
+ // Run validation before Astro's internal validation for build/check commands.
73
78
  try {
74
79
  await getConfig();
75
80
  await validateMdxContent();
@@ -5,6 +5,7 @@
5
5
  "scripts": {
6
6
  "dev": "astro dev",
7
7
  "start": "tsx runner.ts",
8
+ "check": "astro build",
8
9
  "prebuild": "rm -rf public/pagefind",
9
10
  "build": "astro build && node scripts/remove-assistant-for-non-pro.mjs && 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
11
  "preview": "astro preview",
@@ -77,14 +77,14 @@ const assistantHeaderButtonStyle = assistantHeaderThemeColors
77
77
  ? [
78
78
  `--assistant-header-theme-light: ${assistantHeaderThemeColors.light}`,
79
79
  `--assistant-header-theme-dark: ${assistantHeaderThemeColors.dark}`,
80
- `--assistant-header-foreground-light: ${
81
- config.assistant?.icon?.color ??
82
- getButtonForegroundColor("light", assistantHeaderThemeColors.light)
83
- }`,
84
- `--assistant-header-foreground-dark: ${
85
- config.assistant?.icon?.color ??
86
- getButtonForegroundColor("dark", assistantHeaderThemeColors.dark)
87
- }`,
80
+ `--assistant-header-foreground-light: ${getButtonForegroundColor(
81
+ "light",
82
+ assistantHeaderThemeColors.light,
83
+ )}`,
84
+ `--assistant-header-foreground-dark: ${getButtonForegroundColor(
85
+ "dark",
86
+ assistantHeaderThemeColors.dark,
87
+ )}`,
88
88
  ].join("; ")
89
89
  : "";
90
90
 
@@ -22,7 +22,7 @@ const currentPrefix = parentSlug ? `${parentSlug}/${groupSlug}` : groupSlug;
22
22
  <div class:list={["text-sm font-semibold mb-2 flex items-center gap-2 px-2"]}>
23
23
  {item.icon && <Icon name={item.icon} class="size-4 text-neutral-500" />}
24
24
  {item.group}
25
- {item.tag && <Tag>{item.tag}</Tag>}
25
+ {item.tag && <Tag tag={item.tag} />}
26
26
  </div>
27
27
 
28
28
  <ul>
@@ -3,13 +3,13 @@ import { buildMdxPageHref, deriveTitleFromEntryId } from "../lib/utils";
3
3
  import Icon from "./ui/Icon.astro";
4
4
  import Tag from "./ui/Tag.astro";
5
5
  import { getCollection } from "astro:content";
6
- import { getConfig } from "../lib/validation";
6
+ import { getConfig, type NavTag } from "../lib/validation";
7
7
 
8
8
  interface Props {
9
9
  path: string;
10
10
  groupSlug?: string;
11
11
  icon?: string | null;
12
- tag?: string;
12
+ tag?: NavTag;
13
13
  title?: string;
14
14
  }
15
15
 
@@ -61,6 +61,6 @@ const isActive = currentPath === targetPath;
61
61
  <div class="flex items-center gap-2">
62
62
  {icon && <Icon name={icon} class="size-4 opacity-75" />}
63
63
  {text}
64
- {tag && <Tag>{tag}</Tag>}
64
+ {tag && <Tag tag={tag} />}
65
65
  </div>
66
66
  </a>
@@ -82,7 +82,7 @@ const containsActivePage = item.pages.some((child) => {
82
82
  <div class="flex items-center gap-2">
83
83
  {item.icon && <Icon name={item.icon} class="size-4 opacity-75" />}
84
84
  {item.group}
85
- {item.tag && <Tag>{item.tag}</Tag>}
85
+ {item.tag && <Tag tag={item.tag} />}
86
86
  </div>
87
87
  <svg
88
88
  xmlns="http://www.w3.org/2000/svg"
@@ -2,13 +2,14 @@
2
2
  import Tag from "../ui/Tag.astro";
3
3
  import { buildOpenApiEndpointHref } from "../../lib/utils";
4
4
  import { methodColors } from "../../lib/utils";
5
+ import type { NavTag } from "../../lib/validation";
5
6
 
6
7
  interface Props {
7
8
  method: string;
8
9
  path: string;
9
10
  summary?: string;
10
11
  title?: string;
11
- tag?: string;
12
+ tag?: NavTag;
12
13
  parentSlug?: string;
13
14
  }
14
15
 
@@ -49,6 +50,6 @@ const isActive = currentPath === targetPath;
49
50
  </span>
50
51
  <div class="flex items-center gap-2 min-w-0">
51
52
  <span>{text}</span>
52
- {tag && <Tag>{tag}</Tag>}
53
+ {tag && <Tag tag={tag} />}
53
54
  </div>
54
55
  </a>
@@ -1,5 +1,67 @@
1
+ ---
2
+ import {
3
+ getConfig,
4
+ type NavTag,
5
+ type ThemeColorByMode,
6
+ } from "../../lib/validation";
7
+
8
+ interface Props {
9
+ tag?: NavTag;
10
+ color?: string | ThemeColorByMode;
11
+ }
12
+
13
+ const { tag, color } = Astro.props;
14
+ const config = await getConfig();
15
+
16
+ function getTagText(value: NavTag | undefined): string | undefined {
17
+ if (typeof value === "string") return value;
18
+ return value?.text;
19
+ }
20
+
21
+ function getTagColor(
22
+ value: NavTag | undefined,
23
+ ): string | ThemeColorByMode | undefined {
24
+ if (typeof value === "object" && value !== null) {
25
+ return value.color;
26
+ }
27
+ return undefined;
28
+ }
29
+
30
+ function resolveColorByMode(
31
+ value: string | ThemeColorByMode | undefined,
32
+ ): { light: string; dark: string } | undefined {
33
+ if (typeof value === "string") {
34
+ return { light: value, dark: value };
35
+ }
36
+
37
+ const light = value?.light ?? value?.dark;
38
+ const dark = value?.dark ?? value?.light;
39
+ if (!light || !dark) return undefined;
40
+
41
+ return { light, dark };
42
+ }
43
+
44
+ const text = getTagText(tag);
45
+ const configuredColor =
46
+ color ?? getTagColor(tag) ?? config.theme?.tag?.color ?? undefined;
47
+ const resolvedColor = resolveColorByMode(configuredColor);
48
+ const colorStyle = resolvedColor
49
+ ? `--rd-tag-color-light:${resolvedColor.light};--rd-tag-color-dark:${resolvedColor.dark};`
50
+ : undefined;
51
+ ---
52
+
1
53
  <span
2
- class="text-[9px] bg-blue-100/70 text-blue-800/90 border border-blue-800/10 px-1.5 py-px rounded-full tracking-wide font-semibold shrink-0 normal-case"
54
+ class="rd-tag text-[9px] border px-[5px] py-[2px] rounded-full tracking-wide leading-none font-medium shrink-0"
55
+ style={colorStyle}
3
56
  >
4
- <slot />
57
+ {text ? text : <slot />}
5
58
  </span>
59
+
60
+ <style>
61
+ .rd-tag {
62
+ --rd-tag-color: var(--rd-tag-color-light, var(--color-theme));
63
+ background-color: color-mix(in oklab, var(--rd-tag-color) 8%, transparent);
64
+ border-color: color-mix(in oklab, var(--rd-tag-color) 0%, transparent);
65
+ color: color-mix(in oklab, var(--rd-tag-color) 95%, transparent);
66
+ }
67
+ </style>
@@ -282,7 +282,7 @@ const renderedCodeLinesHtml = normalizedTokenLines
282
282
  >
283
283
  <div
284
284
  class:list={[
285
- "w-full max-w-full min-w-0 bg-(--rd-code-surface)",
285
+ "w-full max-w-full min-w-0",
286
286
  parsedInCodeGroup
287
287
  ? "rounded-tr-none rounded-xl"
288
288
  : parsedFlushTop
@@ -98,7 +98,7 @@ const isInitiallyExpanded = shouldShowAllCode || totalLineCount <= visibleLines;
98
98
  >
99
99
  <button
100
100
  type="button"
101
- class="pointer-events-auto inline-flex h-8 items-center justify-center rounded-xl [corner-shape:superellipse(1.2)] border-[0.5px] border-neutral-900/10 dark:border-white/8 bg-linear-to-br from-white via-white/10 to-neutral-900/5 dark:from-white/7 dark:via-white/6 dark:to-white/2 px-3 text-sm font-medium text-neutral-600 hover:text-neutral-900 dark:text-neutral-300/90 hover:dark:text-neutral-200 transition-colors duration-200 hover:bg-neutral-50/80 cursor-pointer dark:hover:bg-neutral-700/30 shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-4px_rgba(0,0,0,0.08)] dark:shadow-[0_-0.5px_0px_rgba(255,255,255,0.15),0_5px_12px_-4px_rgba(0,0,0,0.6)]"
101
+ class="pointer-events-auto inline-flex h-8 items-center justify-center rounded-xl [corner-shape:superellipse(1.2)] border-[0.5px] border-neutral-900/10 dark:border-white/8 bg-linear-to-br from-white via-white/10 to-neutral-900/5 dark:from-white/7 dark:via-white/6 dark:to-white/2 px-3 text-sm font-medium text-neutral-600 hover:text-neutral-900 dark:text-neutral-300/90 hover:dark:text-neutral-200 transition-colors duration-200 hover:bg-neutral-50/80 cursor-pointer dark:hover:bg-neutral-700/30 shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-4px_rgba(0,0,0,0.08)] dark:shadow-[0_-0.5px_0px_rgba(255,255,255,0.15),0_5px_12px_-4px_rgba(0,0,0,0.18)]"
102
102
  data-rd-preview-expand
103
103
  >
104
104
  View code
@@ -114,7 +114,23 @@ function isConstrainedWidthValue(value: unknown): boolean {
114
114
  return true;
115
115
  }
116
116
 
117
+ function toCssWidthValue(value: unknown): string | undefined {
118
+ if (typeof value === "number") {
119
+ if (!Number.isFinite(value) || value <= 0) return undefined;
120
+ return `${value}px`;
121
+ }
122
+
123
+ if (typeof value !== "string") return undefined;
124
+ const normalized = value.trim();
125
+ if (!normalized) return undefined;
126
+
127
+ return /^-?\d*\.?\d+$/.test(normalized) ? `${normalized}px` : normalized;
128
+ }
129
+
117
130
  const hasCustomImageWidth = isConstrainedWidthValue(rawWidth);
131
+ const contentWidthStyle = hasCustomImageWidth
132
+ ? `width: ${toCssWidthValue(rawWidth) ?? "auto"};`
133
+ : undefined;
118
134
 
119
135
  const slotCaptionHtml = Astro.slots.has("default")
120
136
  ? (await Astro.slots.render("default")).trim()
@@ -240,45 +256,46 @@ const hasCaption = slotCaptionHtml.length > 0;
240
256
  }"
241
257
  >
242
258
  <div
243
- class="overflow-hidden rounded-[11px]"
259
+ class:list={["max-w-full", !hasCustomImageWidth && "w-full"]}
260
+ style={contentWidthStyle}
244
261
  >
245
- <img
246
- {...lightImageAttrs}
247
- x-ref="lightImg"
248
- title={title}
249
- class:list={[
250
- "h-auto my-0! block transition-opacity",
251
- hasDarkSource && "dark:hidden",
252
- zoomEnabled && "cursor-zoom-in",
253
- hasCustomImageWidth ? "max-w-full" : "w-full",
254
- ]}
255
- :class="showZoomed ? 'opacity-0 duration-0' : 'opacity-100 duration-300'"
256
- @click={zoomEnabled ? "zoom()" : undefined}
257
- />
262
+ <div class="overflow-hidden rounded-[11px]">
263
+ <img
264
+ {...lightImageAttrs}
265
+ x-ref="lightImg"
266
+ title={title}
267
+ class:list={[
268
+ "h-auto my-0! block w-full transition-opacity",
269
+ hasDarkSource && "dark:hidden",
270
+ zoomEnabled && "cursor-zoom-in",
271
+ ]}
272
+ :class="showZoomed ? 'opacity-0 duration-0' : 'opacity-100 duration-300'"
273
+ @click={zoomEnabled ? "zoom()" : undefined}
274
+ />
275
+ {
276
+ darkImageAttrs && (
277
+ <img
278
+ {...darkImageAttrs}
279
+ x-ref="darkImg"
280
+ title={title}
281
+ class:list={[
282
+ "h-auto my-0! hidden w-full transition-opacity dark:block",
283
+ zoomEnabled && "cursor-zoom-in",
284
+ ]}
285
+ :class="showZoomed ? 'opacity-0 duration-0' : 'opacity-100 duration-300'"
286
+ @click={zoomEnabled ? "zoom()" : undefined}
287
+ />
288
+ )
289
+ }
290
+ </div>
258
291
  {
259
- darkImageAttrs && (
260
- <img
261
- {...darkImageAttrs}
262
- x-ref="darkImg"
263
- title={title}
264
- class:list={[
265
- "h-auto my-0! hidden transition-opacity dark:block",
266
- zoomEnabled && "cursor-zoom-in",
267
- hasCustomImageWidth ? "max-w-full" : "w-full",
268
- ]}
269
- :class="showZoomed ? 'opacity-0 duration-0' : 'opacity-100 duration-300'"
270
- @click={zoomEnabled ? "zoom()" : undefined}
271
- />
292
+ hasCaption && (
293
+ <figcaption class="prose-rules mt-1! xs:mt-1.5! max-w-none! *:max-w-none! text-center text-xs! xs:text-sm! text-neutral-500 dark:text-neutral-400 leading-relaxed px-2">
294
+ <Fragment set:html={slotCaptionHtml} />
295
+ </figcaption>
272
296
  )
273
297
  }
274
298
  </div>
275
- {
276
- hasCaption && (
277
- <figcaption class="prose-rules mt-1! xs:mt-1.5! max-w-none! *:max-w-none! text-center text-xs! xs:text-sm! text-neutral-500 dark:text-neutral-400 leading-relaxed px-2">
278
- <Fragment set:html={slotCaptionHtml} />
279
- </figcaption>
280
- )
281
- }
282
299
 
283
300
  {
284
301
  zoomEnabled && (
@@ -153,18 +153,14 @@ export function getAssistantLauncherIconConfig(
153
153
  dark: getAssistantButtonColor(config, "dark"),
154
154
  };
155
155
  const iconColors = {
156
- light:
157
- assistantConfig?.icon?.color ??
158
- getThemeForegroundColor(
159
- themeColors.light,
160
- getDocsBaseColorShade(config.theme, "light", "900"),
161
- ),
162
- dark:
163
- assistantConfig?.icon?.color ??
164
- getThemeForegroundColor(
165
- themeColors.dark,
166
- getDocsBaseColorShade(config.theme, "dark", "900"),
167
- ),
156
+ light: getThemeForegroundColor(
157
+ themeColors.light,
158
+ getDocsBaseColorShade(config.theme, "light", "900"),
159
+ ),
160
+ dark: getThemeForegroundColor(
161
+ themeColors.dark,
162
+ getDocsBaseColorShade(config.theme, "dark", "900"),
163
+ ),
168
164
  };
169
165
  const icon = resolveLauncherIcon(assistantConfig?.icon?.src);
170
166
 
@@ -297,9 +293,240 @@ export function renderAssistantEmbedScript(config: DocsConfig): string {
297
293
  });
298
294
  }
299
295
 
296
+ function normalizePathForMatching(value) {
297
+ var pathname = String(value || "").trim();
298
+ if (!pathname) return "";
299
+ if (pathname === "*") return "*";
300
+
301
+ try {
302
+ if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(pathname)) {
303
+ pathname = new URL(pathname).pathname;
304
+ }
305
+ } catch {
306
+ // Fall back to treating the value as a path pattern.
307
+ }
308
+
309
+ var hashIndex = pathname.indexOf("#");
310
+ if (hashIndex >= 0) {
311
+ pathname = pathname.slice(0, hashIndex);
312
+ }
313
+
314
+ var queryIndex = pathname.indexOf("?");
315
+ if (queryIndex >= 0) {
316
+ pathname = pathname.slice(0, queryIndex);
317
+ }
318
+
319
+ if (!pathname) return "";
320
+ if (pathname.charAt(0) !== "/") {
321
+ pathname = "/" + pathname;
322
+ }
323
+
324
+ pathname = pathname.replace(/\\/{2,}/g, "/");
325
+ if (pathname.length > 1 && !pathname.endsWith("/*")) {
326
+ pathname = pathname.replace(/\\/+$/, "");
327
+ }
328
+
329
+ return pathname || "/";
330
+ }
331
+
332
+ function parsePathPatterns(value) {
333
+ var rawPatterns = [];
334
+
335
+ if (Array.isArray(value)) {
336
+ rawPatterns = value;
337
+ } else if (typeof value === "string") {
338
+ var trimmed = value.trim();
339
+ if (!trimmed) return [];
340
+
341
+ if (trimmed.charAt(0) === "[") {
342
+ try {
343
+ var parsed = JSON.parse(trimmed);
344
+ if (Array.isArray(parsed)) {
345
+ rawPatterns = parsed;
346
+ } else {
347
+ rawPatterns = [trimmed];
348
+ }
349
+ } catch {
350
+ rawPatterns = trimmed.split(/[\\n,]/);
351
+ }
352
+ } else {
353
+ rawPatterns = trimmed.split(/[\\n,]/);
354
+ }
355
+ }
356
+
357
+ var seenPatterns = {};
358
+ return rawPatterns
359
+ .map(normalizePathForMatching)
360
+ .filter(function (pattern) {
361
+ if (!pattern || seenPatterns[pattern]) return false;
362
+ seenPatterns[pattern] = true;
363
+ return true;
364
+ });
365
+ }
366
+
367
+ function escapeRegExp(value) {
368
+ return String(value).replace(/[|\\\\{}()[\\]^$+?.]/g, "\\\\$&");
369
+ }
370
+
371
+ function pathMatchesPattern(pathname, pattern) {
372
+ if (pattern === "*") return true;
373
+
374
+ if (pattern.endsWith("/*")) {
375
+ var basePath = normalizePathForMatching(pattern.slice(0, -2));
376
+ return pathname === basePath || pathname.startsWith(basePath + "/");
377
+ }
378
+
379
+ if (pattern.indexOf("*") >= 0) {
380
+ var patternRegex = new RegExp(
381
+ "^" + pattern.split("*").map(escapeRegExp).join(".*") + "$",
382
+ );
383
+ return patternRegex.test(pathname);
384
+ }
385
+
386
+ return pathname === pattern;
387
+ }
388
+
389
+ function isCurrentPathExcluded() {
390
+ if (!excludedPathPatterns.length) return false;
391
+ var pathname = normalizePathForMatching(window.location.pathname || "/");
392
+ return excludedPathPatterns.some(function (pattern) {
393
+ return pathMatchesPattern(pathname, pattern);
394
+ });
395
+ }
396
+
397
+ function parseDurationMs(value, fallbackMs) {
398
+ var rawValue = String(value || "").trim();
399
+ var match = rawValue.match(/^([0-9]*\\.?[0-9]+)(ms|s)$/);
400
+ if (!match) return fallbackMs;
401
+
402
+ var amount = Number.parseFloat(match[1]);
403
+ if (!Number.isFinite(amount)) return fallbackMs;
404
+
405
+ return match[2] === "s" ? amount * 1000 : amount;
406
+ }
407
+
408
+ function clearRouteVisibilityRemovalTimeout() {
409
+ if (!routeVisibilityRemovalTimeout) return;
410
+ window.clearTimeout(routeVisibilityRemovalTimeout);
411
+ routeVisibilityRemovalTimeout = undefined;
412
+ }
413
+
414
+ function unmountEmbedElements() {
415
+ clearRouteVisibilityRemovalTimeout();
416
+ setOpen(false);
417
+
418
+ if (launcherButton) {
419
+ launcherButton.remove();
420
+ launcherButton = undefined;
421
+ }
422
+
423
+ if (panelShell) {
424
+ panelShell.remove();
425
+ panelShell = undefined;
426
+ }
427
+
428
+ panelFrame = undefined;
429
+ }
430
+
431
+ function hideEmbedElementsForRouteExclusion() {
432
+ if (!panelShell && !launcherButton) {
433
+ return;
434
+ }
435
+
436
+ clearRouteVisibilityRemovalTimeout();
437
+ setOpen(false);
438
+
439
+ if (launcherButton) {
440
+ launcherButton.dataset.routeHidden = "true";
441
+ }
442
+
443
+ routeVisibilityRemovalTimeout = window.setTimeout(function () {
444
+ routeVisibilityRemovalTimeout = undefined;
445
+ if (isCurrentPathExcluded()) {
446
+ unmountEmbedElements();
447
+ }
448
+ }, routeVisibilityRemovalDelayMs);
449
+ }
450
+
451
+ function applyRouteVisibility() {
452
+ if (isCurrentPathExcluded()) {
453
+ hideEmbedElementsForRouteExclusion();
454
+ return;
455
+ }
456
+
457
+ mount();
458
+ }
459
+
460
+ function scheduleRouteVisibilityCheck() {
461
+ window.setTimeout(applyRouteVisibility, 0);
462
+ }
463
+
464
+ function installRouteVisibilityListeners() {
465
+ if (!excludedPathPatterns.length || routeVisibilityListenersInstalled) {
466
+ return;
467
+ }
468
+
469
+ routeVisibilityListenersInstalled = true;
470
+
471
+ if (window.history && typeof window.history.pushState === "function") {
472
+ originalHistoryPushState = window.history.pushState;
473
+ patchedHistoryPushState = function () {
474
+ var result = originalHistoryPushState.apply(this, arguments);
475
+ scheduleRouteVisibilityCheck();
476
+ return result;
477
+ };
478
+ window.history.pushState = patchedHistoryPushState;
479
+ }
480
+
481
+ if (window.history && typeof window.history.replaceState === "function") {
482
+ originalHistoryReplaceState = window.history.replaceState;
483
+ patchedHistoryReplaceState = function () {
484
+ var result = originalHistoryReplaceState.apply(this, arguments);
485
+ scheduleRouteVisibilityCheck();
486
+ return result;
487
+ };
488
+ window.history.replaceState = patchedHistoryReplaceState;
489
+ }
490
+
491
+ window.addEventListener("popstate", scheduleRouteVisibilityCheck);
492
+ window.addEventListener("hashchange", scheduleRouteVisibilityCheck);
493
+ }
494
+
495
+ function restoreRouteVisibilityListeners() {
496
+ if (!routeVisibilityListenersInstalled) {
497
+ return;
498
+ }
499
+
500
+ if (
501
+ window.history &&
502
+ patchedHistoryPushState &&
503
+ window.history.pushState === patchedHistoryPushState
504
+ ) {
505
+ window.history.pushState = originalHistoryPushState;
506
+ }
507
+
508
+ if (
509
+ window.history &&
510
+ patchedHistoryReplaceState &&
511
+ window.history.replaceState === patchedHistoryReplaceState
512
+ ) {
513
+ window.history.replaceState = originalHistoryReplaceState;
514
+ }
515
+
516
+ window.removeEventListener("popstate", scheduleRouteVisibilityCheck);
517
+ window.removeEventListener("hashchange", scheduleRouteVisibilityCheck);
518
+ routeVisibilityListenersInstalled = false;
519
+ }
520
+
300
521
  var docsOrigin = resolveDocsOrigin();
301
522
  var docsBasePath = resolveDocsBasePath();
302
523
  var chrome = generatedConfig.chrome || {};
524
+ var excludedPathPatterns = parsePathPatterns(
525
+ scriptDataset.excludePaths ||
526
+ globalConfig.excludePaths ||
527
+ globalConfig.excludedPaths ||
528
+ "",
529
+ );
303
530
  var zIndex = scriptDataset.zIndex || chrome.zIndex || "2147483000";
304
531
  var configuredMode =
305
532
  scriptDataset.mode ||
@@ -339,6 +566,7 @@ export function renderAssistantEmbedScript(config: DocsConfig): string {
339
566
  var panelSizeTiming = chrome.panelSizeTiming || "cubic-bezier(.22,1.18,.36,1)";
340
567
  var panelCloseDuration = chrome.panelCloseDuration || ".2s";
341
568
  var panelCloseTiming = chrome.panelCloseTiming || "cubic-bezier(.25,1,.5,1)";
569
+ var launcherRouteExitDurationMs = 460;
342
570
  var mobileBreakpoint = chrome.mobileBreakpoint || "640px";
343
571
  var panelFullscreenHeightBreakpoint =
344
572
  chrome.panelFullscreenHeightBreakpoint || "560px";
@@ -367,6 +595,17 @@ export function renderAssistantEmbedScript(config: DocsConfig): string {
367
595
  var panelSize = readPanelSize();
368
596
  var panelSizeTransitionTimeout;
369
597
  var isPanelFullscreen = false;
598
+ var routeVisibilityRemovalTimeout;
599
+ var routeVisibilityRemovalDelayMs =
600
+ Math.max(
601
+ parseDurationMs(panelCloseDuration, 200),
602
+ launcherRouteExitDurationMs,
603
+ ) + 80;
604
+ var routeVisibilityListenersInstalled = false;
605
+ var originalHistoryPushState;
606
+ var originalHistoryReplaceState;
607
+ var patchedHistoryPushState;
608
+ var patchedHistoryReplaceState;
370
609
  var systemThemeMediaQuery =
371
610
  typeof window.matchMedia === "function"
372
611
  ? window.matchMedia("(prefers-color-scheme: dark)")
@@ -508,9 +747,11 @@ export function renderAssistantEmbedScript(config: DocsConfig): string {
508
747
  style.textContent = [
509
748
  ".assistant-embed-frame{display:block;width:100%;height:100%;border:0;background:transparent;color-scheme:normal;}",
510
749
  "@keyframes assistant-embed-launcher-enter{0%{opacity:0;transform:translateY(3px) scale(.96);}46%{opacity:1;transform:translateY(0) scale(1.012);}72%{opacity:1;transform:translateY(0) scale(.998);}100%{opacity:1;transform:translateY(0) scale(1);}}",
750
+ "@keyframes assistant-embed-launcher-exit{0%{opacity:1;transform:translateY(0) scale(1);}28%{opacity:1;transform:translateY(0) scale(.998);}54%{opacity:1;transform:translateY(0) scale(1.012);}100%{opacity:0;transform:translateY(3px) scale(.96);}}",
511
751
  ".assistant-embed-launcher{position:fixed;display:inline-flex;align-items:center;justify-content:center;border:0;padding:0;cursor:pointer;color:var(--assistant-embed-icon-color);background:linear-gradient(to bottom,color-mix(in oklab,var(--assistant-embed-theme) 88%,white),color-mix(in oklab,var(--assistant-embed-theme) 90%,black));animation:assistant-embed-launcher-enter 460ms linear backwards;transition:transform 180ms ease,opacity 160ms ease;transform-origin:center center;}",
512
752
  ".assistant-embed-launcher:hover{opacity:.96;transform:translateY(-1px) scale(1.03);}",
513
753
  ".assistant-embed-launcher:active{transform:translateY(0) scale(.96);}",
754
+ ".assistant-embed-launcher[data-route-hidden='true']{pointer-events:none;animation:assistant-embed-launcher-exit 460ms linear forwards;}",
514
755
  ".assistant-embed-launcher{width:" +
515
756
  launcherSize +
516
757
  ";height:" +
@@ -883,6 +1124,10 @@ export function renderAssistantEmbedScript(config: DocsConfig): string {
883
1124
  }
884
1125
 
885
1126
  function setOpen(nextOpen) {
1127
+ if (nextOpen === true && isCurrentPathExcluded()) {
1128
+ return;
1129
+ }
1130
+
886
1131
  var wasOpen = isOpen;
887
1132
  isOpen = nextOpen === true;
888
1133
  setPanelOpen(isOpen);
@@ -972,6 +1217,25 @@ export function renderAssistantEmbedScript(config: DocsConfig): string {
972
1217
  return;
973
1218
  }
974
1219
 
1220
+ if (isCurrentPathExcluded()) {
1221
+ hideEmbedElementsForRouteExclusion();
1222
+ return;
1223
+ }
1224
+
1225
+ clearRouteVisibilityRemovalTimeout();
1226
+
1227
+ if (launcherButton) {
1228
+ delete launcherButton.dataset.routeHidden;
1229
+ }
1230
+
1231
+ if (panelShell && launcherButton) {
1232
+ return;
1233
+ }
1234
+
1235
+ if (panelShell || launcherButton) {
1236
+ unmountEmbedElements();
1237
+ }
1238
+
975
1239
  ensureStyle();
976
1240
  updatePanelLayout();
977
1241
 
@@ -1063,16 +1327,12 @@ export function renderAssistantEmbedScript(config: DocsConfig): string {
1063
1327
  },
1064
1328
  destroy: function () {
1065
1329
  setSystemThemeListener(false);
1330
+ restoreRouteVisibilityListeners();
1066
1331
  window.removeEventListener("resize", updatePanelLayout);
1067
1332
  if (panelSizeTransitionTimeout) {
1068
1333
  window.clearTimeout(panelSizeTransitionTimeout);
1069
1334
  }
1070
- if (launcherButton) {
1071
- launcherButton.remove();
1072
- }
1073
- if (panelShell) {
1074
- panelShell.remove();
1075
- }
1335
+ unmountEmbedElements();
1076
1336
  var style = document.getElementById(styleId);
1077
1337
  if (style) {
1078
1338
  style.remove();
@@ -1082,6 +1342,7 @@ export function renderAssistantEmbedScript(config: DocsConfig): string {
1082
1342
  };
1083
1343
 
1084
1344
  setSystemThemeListener(themeMode === "system");
1345
+ installRouteVisibilityListeners();
1085
1346
  mount();
1086
1347
  })();
1087
1348
  `;
@@ -131,7 +131,7 @@ const INTERNAL_ONLY_COMPONENTS = new Set(["ComponentPreview"]);
131
131
  export type NavPage = {
132
132
  page: string;
133
133
  icon?: string | null;
134
- tag?: string;
134
+ tag?: NavTag;
135
135
  title?: string;
136
136
  };
137
137
  export type NavOpenApiPageRef = {
@@ -141,14 +141,14 @@ export type NavOpenApiPageRef = {
141
141
  export type NavOpenApiPage = {
142
142
  openapi: NavOpenApiPageRef;
143
143
  title?: string;
144
- tag?: string;
144
+ tag?: NavTag;
145
145
  };
146
146
  export type NavGroup = {
147
147
  group: string;
148
148
  pages: (string | NavPage | NavGroup | NavOpenApiPage)[];
149
149
  icon?: string | null;
150
150
  expanded?: boolean; // need to add this logic
151
- tag?: string;
151
+ tag?: NavTag;
152
152
  };
153
153
  export type NavOpenApi = {
154
154
  source: string;
@@ -199,6 +199,16 @@ export type Logo = {
199
199
  href?: string;
200
200
  pill?: string | false;
201
201
  };
202
+ export type ThemeColorByMode = {
203
+ light?: string;
204
+ dark?: string;
205
+ };
206
+ export type NavTag =
207
+ | string
208
+ | {
209
+ text: string;
210
+ color?: string | ThemeColorByMode;
211
+ };
202
212
  export const BASE_COLOR_OPTIONS = [
203
213
  "slate",
204
214
  "gray",
@@ -217,10 +227,6 @@ export type BaseColorByMode = {
217
227
  };
218
228
  export const DEFAULT_THEME_COLOR_LIGHT = "#171717";
219
229
  export const DEFAULT_THEME_COLOR_DARK = "#f5f5f5";
220
- export type ThemeColorByMode = {
221
- light?: string;
222
- dark?: string;
223
- };
224
230
  export type CardCoverTheme = {
225
231
  colors?: string[];
226
232
  colorSeed?: string;
@@ -241,15 +247,18 @@ export type CodeSyntaxThemeConfig =
241
247
  export type CodeTheme = {
242
248
  syntaxTheme?: CodeSyntaxThemeConfig;
243
249
  };
250
+ export type TagTheme = {
251
+ color?: string | ThemeColorByMode;
252
+ };
244
253
  export type DocsTheme = {
245
254
  baseColor?: BaseColorOption | BaseColorByMode;
246
255
  themeColor?: string | ThemeColorByMode;
247
256
  card?: CardTheme;
248
257
  code?: CodeTheme;
258
+ tag?: TagTheme;
249
259
  };
250
260
  export type AssistantIcon = {
251
261
  src?: string;
252
- color?: string;
253
262
  };
254
263
  export type AssistantButtonSize = "small" | "default";
255
264
  export type AssistantButtonConfig = {
@@ -428,6 +437,71 @@ function normalizeThemeColorConfig(
428
437
  };
429
438
  }
430
439
 
440
+ function normalizeNavTagConfig(
441
+ value: unknown,
442
+ currentPath: Path,
443
+ label: string,
444
+ ): NavTag | undefined {
445
+ if (value === undefined || value === null) return undefined;
446
+
447
+ if (typeof value === "string") {
448
+ const trimmedText = value.trim();
449
+ if (trimmedText.length === 0) {
450
+ throwConfigError(`${label} cannot be empty.`, currentPath);
451
+ }
452
+ return trimmedText;
453
+ }
454
+
455
+ checkType(value, "object", currentPath, label);
456
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
457
+ throwConfigError(
458
+ `${label} must be a string or an object with text and optional color.`,
459
+ currentPath,
460
+ );
461
+ }
462
+
463
+ const tagConfig = value as Record<string, unknown>;
464
+ const allowedKeys = new Set(["text", "color"]);
465
+ for (const key of Object.keys(tagConfig)) {
466
+ if (!allowedKeys.has(key)) {
467
+ throwConfigError(`${label} object only supports 'text' and 'color'.`, [
468
+ ...currentPath,
469
+ key,
470
+ ]);
471
+ }
472
+ }
473
+
474
+ checkType(tagConfig.text, "string", [...currentPath, "text"], `${label} text`);
475
+ if (typeof tagConfig.text !== "string") {
476
+ throwConfigError(`${label} text must be a string.`, [
477
+ ...currentPath,
478
+ "text",
479
+ ]);
480
+ }
481
+
482
+ const trimmedText = tagConfig.text.trim();
483
+ if (trimmedText.length === 0) {
484
+ throwConfigError(`${label} text cannot be empty.`, [
485
+ ...currentPath,
486
+ "text",
487
+ ]);
488
+ }
489
+
490
+ const color =
491
+ tagConfig.color !== undefined
492
+ ? normalizeThemeColorConfig(
493
+ tagConfig.color,
494
+ [...currentPath, "color"],
495
+ `${label} color`,
496
+ )
497
+ : undefined;
498
+
499
+ return {
500
+ text: trimmedText,
501
+ ...(color !== undefined ? { color } : {}),
502
+ };
503
+ }
504
+
431
505
  function normalizeHexColorArray(
432
506
  value: unknown,
433
507
  currentPath: Path,
@@ -909,6 +983,7 @@ async function validateNavigationNode(
909
983
  checkType(item.expanded, "boolean", [...path, "expanded"], "Expanded");
910
984
 
911
985
  validateIcon(item.icon, [...path, "icon"]);
986
+ item.tag = normalizeNavTagConfig(item.tag, [...path, "tag"], "Group tag");
912
987
 
913
988
  // Check if pages array exists and validate children
914
989
  if (!item.pages)
@@ -947,6 +1022,7 @@ async function validateNavigationNode(
947
1022
 
948
1023
  // Validate optional title
949
1024
  checkType(item.title, "string", [...path, "title"], "Page title");
1025
+ item.tag = normalizeNavTagConfig(item.tag, [...path, "tag"], "Page tag");
950
1026
 
951
1027
  // Check D.2/D.3: Page cannot have group properties
952
1028
  if ("expanded" in item)
@@ -971,7 +1047,11 @@ async function validateNavigationNode(
971
1047
 
972
1048
  await validateNavOpenApiPage(item.openapi, [...path, "openapi"]);
973
1049
  checkType(item.title, "string", [...path, "title"], "Open API page title");
974
- checkType(item.tag, "string", [...path, "tag"], "Open API page tag");
1050
+ item.tag = normalizeNavTagConfig(
1051
+ item.tag,
1052
+ [...path, "tag"],
1053
+ "Open API page tag",
1054
+ );
975
1055
 
976
1056
  if ("expanded" in item)
977
1057
  throwConfigError("Open API page items cannot have 'expanded'.", [
@@ -1855,6 +1935,37 @@ function validateTheme(theme: DocsConfig["theme"]): void {
1855
1935
  }
1856
1936
  }
1857
1937
 
1938
+ if (theme.tag !== undefined) {
1939
+ checkType(theme.tag, "object", ["theme", "tag"], "Theme tag");
1940
+ if (
1941
+ typeof theme.tag !== "object" ||
1942
+ theme.tag === null ||
1943
+ Array.isArray(theme.tag)
1944
+ ) {
1945
+ throwConfigError("Theme tag must be an object.", ["theme", "tag"]);
1946
+ }
1947
+
1948
+ const tagTheme = theme.tag as TagTheme & Record<string, unknown>;
1949
+ const allowedTagKeys = new Set(["color"]);
1950
+ for (const key of Object.keys(tagTheme)) {
1951
+ if (!allowedTagKeys.has(key)) {
1952
+ throwConfigError("Theme tag configuration only supports 'color'.", [
1953
+ "theme",
1954
+ "tag",
1955
+ key,
1956
+ ]);
1957
+ }
1958
+ }
1959
+
1960
+ if (tagTheme.color !== undefined) {
1961
+ tagTheme.color = normalizeThemeColorConfig(
1962
+ tagTheme.color,
1963
+ ["theme", "tag", "color"],
1964
+ "Theme tag color",
1965
+ );
1966
+ }
1967
+ }
1968
+
1858
1969
  if (theme.card !== undefined) {
1859
1970
  checkType(theme.card, "object", ["theme", "card"], "Theme card");
1860
1971
  if (
@@ -2293,22 +2404,19 @@ function validateAssistant(assistant: DocsConfig["assistant"]): void {
2293
2404
  ]);
2294
2405
  }
2295
2406
 
2296
- const allowedIconKeys = new Set(["src", "color"]);
2407
+ const allowedIconKeys = new Set(["src"]);
2297
2408
  for (const key of Object.keys(assistant.icon)) {
2298
2409
  if (!allowedIconKeys.has(key)) {
2299
2410
  throwConfigError(
2300
- "Assistant icon only supports 'src' and 'color'.",
2411
+ "Assistant icon only supports 'src'.",
2301
2412
  ["assistant", "icon", key],
2302
2413
  );
2303
2414
  }
2304
2415
  }
2305
2416
 
2306
- if (
2307
- assistant.icon.src === undefined &&
2308
- assistant.icon.color === undefined
2309
- ) {
2417
+ if (assistant.icon.src === undefined) {
2310
2418
  throwConfigError(
2311
- "Assistant icon must include 'src', 'color', or both.",
2419
+ "Assistant icon must include 'src'.",
2312
2420
  ["assistant", "icon"],
2313
2421
  );
2314
2422
  }
@@ -2320,13 +2428,6 @@ function validateAssistant(assistant: DocsConfig["assistant"]): void {
2320
2428
  );
2321
2429
  }
2322
2430
 
2323
- if (assistant.icon.color !== undefined) {
2324
- assistant.icon.color = normalizeHexColor(
2325
- assistant.icon.color,
2326
- ["assistant", "icon", "color"],
2327
- "Assistant icon color",
2328
- );
2329
- }
2330
2431
  }
2331
2432
 
2332
2433
  function validateHome(home: DocsConfig["home"]): string | undefined {