radiant-docs 0.1.49 → 0.1.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "radiant-docs",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
4
4
  "description": "CLI tool for previewing Radiant documentation locally",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 && (
@@ -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,11 +247,15 @@ 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;
@@ -428,6 +438,71 @@ function normalizeThemeColorConfig(
428
438
  };
429
439
  }
430
440
 
441
+ function normalizeNavTagConfig(
442
+ value: unknown,
443
+ currentPath: Path,
444
+ label: string,
445
+ ): NavTag | undefined {
446
+ if (value === undefined || value === null) return undefined;
447
+
448
+ if (typeof value === "string") {
449
+ const trimmedText = value.trim();
450
+ if (trimmedText.length === 0) {
451
+ throwConfigError(`${label} cannot be empty.`, currentPath);
452
+ }
453
+ return trimmedText;
454
+ }
455
+
456
+ checkType(value, "object", currentPath, label);
457
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
458
+ throwConfigError(
459
+ `${label} must be a string or an object with text and optional color.`,
460
+ currentPath,
461
+ );
462
+ }
463
+
464
+ const tagConfig = value as Record<string, unknown>;
465
+ const allowedKeys = new Set(["text", "color"]);
466
+ for (const key of Object.keys(tagConfig)) {
467
+ if (!allowedKeys.has(key)) {
468
+ throwConfigError(`${label} object only supports 'text' and 'color'.`, [
469
+ ...currentPath,
470
+ key,
471
+ ]);
472
+ }
473
+ }
474
+
475
+ checkType(tagConfig.text, "string", [...currentPath, "text"], `${label} text`);
476
+ if (typeof tagConfig.text !== "string") {
477
+ throwConfigError(`${label} text must be a string.`, [
478
+ ...currentPath,
479
+ "text",
480
+ ]);
481
+ }
482
+
483
+ const trimmedText = tagConfig.text.trim();
484
+ if (trimmedText.length === 0) {
485
+ throwConfigError(`${label} text cannot be empty.`, [
486
+ ...currentPath,
487
+ "text",
488
+ ]);
489
+ }
490
+
491
+ const color =
492
+ tagConfig.color !== undefined
493
+ ? normalizeThemeColorConfig(
494
+ tagConfig.color,
495
+ [...currentPath, "color"],
496
+ `${label} color`,
497
+ )
498
+ : undefined;
499
+
500
+ return {
501
+ text: trimmedText,
502
+ ...(color !== undefined ? { color } : {}),
503
+ };
504
+ }
505
+
431
506
  function normalizeHexColorArray(
432
507
  value: unknown,
433
508
  currentPath: Path,
@@ -909,6 +984,7 @@ async function validateNavigationNode(
909
984
  checkType(item.expanded, "boolean", [...path, "expanded"], "Expanded");
910
985
 
911
986
  validateIcon(item.icon, [...path, "icon"]);
987
+ item.tag = normalizeNavTagConfig(item.tag, [...path, "tag"], "Group tag");
912
988
 
913
989
  // Check if pages array exists and validate children
914
990
  if (!item.pages)
@@ -947,6 +1023,7 @@ async function validateNavigationNode(
947
1023
 
948
1024
  // Validate optional title
949
1025
  checkType(item.title, "string", [...path, "title"], "Page title");
1026
+ item.tag = normalizeNavTagConfig(item.tag, [...path, "tag"], "Page tag");
950
1027
 
951
1028
  // Check D.2/D.3: Page cannot have group properties
952
1029
  if ("expanded" in item)
@@ -971,7 +1048,11 @@ async function validateNavigationNode(
971
1048
 
972
1049
  await validateNavOpenApiPage(item.openapi, [...path, "openapi"]);
973
1050
  checkType(item.title, "string", [...path, "title"], "Open API page title");
974
- checkType(item.tag, "string", [...path, "tag"], "Open API page tag");
1051
+ item.tag = normalizeNavTagConfig(
1052
+ item.tag,
1053
+ [...path, "tag"],
1054
+ "Open API page tag",
1055
+ );
975
1056
 
976
1057
  if ("expanded" in item)
977
1058
  throwConfigError("Open API page items cannot have 'expanded'.", [
@@ -1855,6 +1936,37 @@ function validateTheme(theme: DocsConfig["theme"]): void {
1855
1936
  }
1856
1937
  }
1857
1938
 
1939
+ if (theme.tag !== undefined) {
1940
+ checkType(theme.tag, "object", ["theme", "tag"], "Theme tag");
1941
+ if (
1942
+ typeof theme.tag !== "object" ||
1943
+ theme.tag === null ||
1944
+ Array.isArray(theme.tag)
1945
+ ) {
1946
+ throwConfigError("Theme tag must be an object.", ["theme", "tag"]);
1947
+ }
1948
+
1949
+ const tagTheme = theme.tag as TagTheme & Record<string, unknown>;
1950
+ const allowedTagKeys = new Set(["color"]);
1951
+ for (const key of Object.keys(tagTheme)) {
1952
+ if (!allowedTagKeys.has(key)) {
1953
+ throwConfigError("Theme tag configuration only supports 'color'.", [
1954
+ "theme",
1955
+ "tag",
1956
+ key,
1957
+ ]);
1958
+ }
1959
+ }
1960
+
1961
+ if (tagTheme.color !== undefined) {
1962
+ tagTheme.color = normalizeThemeColorConfig(
1963
+ tagTheme.color,
1964
+ ["theme", "tag", "color"],
1965
+ "Theme tag color",
1966
+ );
1967
+ }
1968
+ }
1969
+
1858
1970
  if (theme.card !== undefined) {
1859
1971
  checkType(theme.card, "object", ["theme", "card"], "Theme card");
1860
1972
  if (