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
@@ -0,0 +1,95 @@
1
+ ---
2
+ import "../../styles/global.css";
3
+ import "../../styles/google-sans-flex.css";
4
+ import "../../styles/geist-mono.css";
5
+ import AssistantEmbedPanel from "./AssistantEmbedPanel";
6
+ import { getAssistantPanelRuntimeConfig } from "../../lib/assistant-panel-config";
7
+ import { getDocsThemeCss } from "../../lib/theme-css";
8
+ import { getConfig } from "../../lib/validation";
9
+
10
+ const config = await getConfig();
11
+ const themeCss = getDocsThemeCss(config.theme);
12
+ const assistantConfig = getAssistantPanelRuntimeConfig(config);
13
+ ---
14
+
15
+ <!doctype html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="utf-8" />
19
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
20
+ <meta name="robots" content="noindex" />
21
+ <style is:inline is:global set:html={themeCss}></style>
22
+ <script is:inline>
23
+ (() => {
24
+ const applyTheme = (theme) => {
25
+ if (theme !== "light" && theme !== "dark") {
26
+ return;
27
+ }
28
+
29
+ document.documentElement.dataset.theme = theme;
30
+ document.documentElement.classList.toggle("dark", theme === "dark");
31
+ };
32
+
33
+ const params = new URLSearchParams(window.location.search);
34
+ const forcedTheme = params.get("mode");
35
+ const localStorageTheme = localStorage.getItem("theme");
36
+ const prefersDark = window.matchMedia(
37
+ "(prefers-color-scheme: dark)",
38
+ ).matches;
39
+ const theme =
40
+ forcedTheme === "light" || forcedTheme === "dark"
41
+ ? forcedTheme
42
+ : localStorageTheme === "light" || localStorageTheme === "dark"
43
+ ? localStorageTheme
44
+ : prefersDark
45
+ ? "dark"
46
+ : "light";
47
+
48
+ applyTheme(theme);
49
+
50
+ window.addEventListener("message", (event) => {
51
+ if (
52
+ !event.data ||
53
+ typeof event.data !== "object" ||
54
+ event.data.type !== "assistant-embed:set-theme"
55
+ ) {
56
+ return;
57
+ }
58
+
59
+ applyTheme(event.data.theme);
60
+ });
61
+ })();
62
+ </script>
63
+ <style is:inline>
64
+ html,
65
+ body {
66
+ width: 100%;
67
+ height: 100%;
68
+ margin: 0;
69
+ overflow: hidden;
70
+ background: transparent;
71
+ }
72
+
73
+ body {
74
+ color-scheme: inherit;
75
+ }
76
+
77
+ @media (max-width: 640px) {
78
+ .assistant-panel-shell > div {
79
+ border-radius: 0;
80
+ border-left: 0;
81
+ border-right: 0;
82
+ }
83
+ }
84
+ </style>
85
+ </head>
86
+ <body data-pagefind-ignore>
87
+ <div class="assistant-panel-shell h-full">
88
+ <AssistantEmbedPanel
89
+ client:only="preact"
90
+ {...assistantConfig}
91
+ linkTarget="blank"
92
+ />
93
+ </div>
94
+ </body>
95
+ </html>
@@ -50,7 +50,7 @@ validateProps(
50
50
  }`
51
51
  x-init="id = (typeof register === 'function') ? register() : Math.random()"
52
52
  role="region"
53
- class="block border-b border-neutral-800/10 dark:border-neutral-700/50 last:border-b-0. [&:first-child>h4>button]:pt-0 [&:last-child>div>div]:pb-0"
53
+ class="rd-accordion block border-b border-neutral-800/10 dark:border-neutral-700/50 last:border-b-0. [&:first-child>h4>button]:pt-0 [&:last-child>div>div]:pb-0"
54
54
  >
55
55
  <h4 class="not-prose">
56
56
  <button
@@ -71,7 +71,7 @@ validateProps(
71
71
  </button>
72
72
  </h4>
73
73
  <div x-show="expanded" x-collapse>
74
- <div class="pb-4! *:first:mt-0! *:last:mb-0!">
74
+ <div class="prose-rules max-w-none! *:max-w-none! pb-4!">
75
75
  <slot />
76
76
  </div>
77
77
  </div>
@@ -7,7 +7,7 @@
7
7
  return this.count;
8
8
  }
9
9
  }"
10
- class="my-5 w-full"
10
+ class="rd-prose-block w-full"
11
11
  >
12
12
  <slot />
13
13
  </div>
@@ -72,13 +72,13 @@ const resolvedTitle = title ?? defaults.title;
72
72
 
73
73
  <aside
74
74
  class:list={[
75
- "my-5 space-y-1 rounded-lg border border-neutral-200 bg-white/80 shadow-xs px-4 py-3.5 pl-6 relative dark:border-neutral-800 dark:bg-(--rd-code-surface)",
75
+ "rd-prose-block relative space-y-1 rounded-xl border-[0.5px]. border-neutral-900/8 bg-white px-4 py-3.5 pl-6 dark:border-neutral-800 dark:bg-(--rd-code-surface) shadow-[0px_1px_3px_0px_rgba(0,0,0,0.04),0px_0px_0px_0.8px_rgba(0,0,0,0.06)_inset,0px_-1px_0px_0px_rgba(0,0,0,0.06)_inset]",
76
76
  ]}
77
77
  role="note"
78
78
  >
79
79
  <div
80
80
  class:list={[
81
- "absolute left-2 inset-y-2 w-[2.5px] rounded-full",
81
+ "absolute left-2.5 inset-y-2.5 w-[2px] rounded-full mb-0",
82
82
  defaults.color,
83
83
  ]}
84
84
  style={color && { backgroundColor: color }}
@@ -96,11 +96,17 @@ const resolvedTitle = title ?? defaults.title;
96
96
  <img src={defaults.icon} alt="" width="16" height="16" class="" />
97
97
  )
98
98
  }
99
- <h6 class:list={["font-semibold text-sm text-neutral-900 dark:text-neutral-100"]}>
99
+ <h6
100
+ class:list={[
101
+ "font-semibold text-sm text-neutral-900 dark:text-neutral-100",
102
+ ]}
103
+ >
100
104
  {resolvedTitle}
101
105
  </h6>
102
106
  </div>
103
- <div class="**:first:mt-0! **:last:mb-0! text-sm text-neutral-700 dark:text-neutral-300">
107
+ <div
108
+ class="prose-rules prose-sm! max-w-none! *:max-w-none! text-neutral-700 dark:text-neutral-300"
109
+ >
104
110
  <slot />
105
111
  </div>
106
112
  </aside>
@@ -0,0 +1,488 @@
1
+ ---
2
+ import { withBasePath } from "../../lib/base-path";
3
+ import { getConfig, type ThemeColorByMode } from "../../lib/validation";
4
+ import Icon from "../ui/Icon.astro";
5
+ import {
6
+ validateNoUnknownProps,
7
+ validateProps,
8
+ } from "../../lib/component-error";
9
+ import {
10
+ getDocsBaseColorShade,
11
+ getThemeForegroundColor,
12
+ } from "../../lib/theme-css";
13
+
14
+ interface Cover {
15
+ icon?: string;
16
+ text?: string;
17
+ glass?: boolean;
18
+ colors?: string[];
19
+ patternSeed?: string;
20
+ colorSeed?: string;
21
+ }
22
+
23
+ interface CardButton {
24
+ text: string;
25
+ href: string;
26
+ color?: string | ThemeColorByMode;
27
+ }
28
+
29
+ interface Props {
30
+ title: string;
31
+ href?: string;
32
+ icon?: string;
33
+ cover?: Cover;
34
+ button?: CardButton;
35
+ }
36
+
37
+ const cardProps = Astro.props as Record<string, unknown>;
38
+
39
+ validateNoUnknownProps(
40
+ "Card",
41
+ cardProps,
42
+ ["title", "href", "icon", "cover", "button"],
43
+ Astro.url.pathname,
44
+ );
45
+
46
+ validateProps(
47
+ "Card",
48
+ cardProps,
49
+ {
50
+ title: { required: true, type: "string" },
51
+ href: { type: "string" },
52
+ icon: { type: "string" },
53
+ cover: { type: "object" },
54
+ button: { type: "object" },
55
+ },
56
+ Astro.url.pathname,
57
+ );
58
+
59
+ const rawCover = cardProps.cover;
60
+ const rawButton = cardProps.button;
61
+ if (rawCover !== undefined && (rawCover === null || Array.isArray(rawCover))) {
62
+ throw new Error(
63
+ `[USER_ERROR]: <Card>: Invalid prop "cover": expected object (in ${Astro.url.pathname})`,
64
+ );
65
+ }
66
+ if (
67
+ rawButton !== undefined &&
68
+ (rawButton === null || Array.isArray(rawButton))
69
+ ) {
70
+ throw new Error(
71
+ `[USER_ERROR]: <Card>: Invalid prop "button": expected object (in ${Astro.url.pathname})`,
72
+ );
73
+ }
74
+
75
+ const coverProps =
76
+ rawCover && typeof rawCover === "object"
77
+ ? (rawCover as Record<string, unknown>)
78
+ : undefined;
79
+ const buttonProps =
80
+ rawButton && typeof rawButton === "object"
81
+ ? (rawButton as Record<string, unknown>)
82
+ : undefined;
83
+
84
+ if (coverProps) {
85
+ validateNoUnknownProps(
86
+ "Card.cover",
87
+ coverProps,
88
+ ["icon", "text", "glass", "colors", "patternSeed", "colorSeed"],
89
+ Astro.url.pathname,
90
+ );
91
+ validateProps(
92
+ "Card.cover",
93
+ coverProps,
94
+ {
95
+ icon: { type: "string" },
96
+ text: { type: "string" },
97
+ glass: { type: "boolean" },
98
+ colors: { type: "array" },
99
+ patternSeed: { type: "string" },
100
+ colorSeed: { type: "string" },
101
+ },
102
+ Astro.url.pathname,
103
+ );
104
+ }
105
+
106
+ if (buttonProps) {
107
+ validateNoUnknownProps(
108
+ "Card.button",
109
+ buttonProps,
110
+ ["text", "href", "color"],
111
+ Astro.url.pathname,
112
+ );
113
+ validateProps(
114
+ "Card.button",
115
+ buttonProps,
116
+ {
117
+ text: { required: true, type: "string" },
118
+ href: { required: true, type: "string" },
119
+ color: { type: ["string", "object"] },
120
+ },
121
+ Astro.url.pathname,
122
+ );
123
+ }
124
+
125
+ function normalizeHexColor(value: unknown, propPath: string): string {
126
+ if (typeof value !== "string") {
127
+ throw new Error(
128
+ `[USER_ERROR]: <Card>: Invalid prop "${propPath}": expected string (in ${Astro.url.pathname})`,
129
+ );
130
+ }
131
+
132
+ const trimmedValue = value.trim();
133
+ const normalizedValue = trimmedValue.startsWith("#")
134
+ ? trimmedValue
135
+ : `#${trimmedValue}`;
136
+ if (
137
+ !/^#(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(
138
+ normalizedValue,
139
+ )
140
+ ) {
141
+ throw new Error(
142
+ `[USER_ERROR]: <Card>: Invalid prop "${propPath}": expected a hex color like "#3b82f6" (in ${Astro.url.pathname})`,
143
+ );
144
+ }
145
+
146
+ return normalizedValue.toLowerCase();
147
+ }
148
+
149
+ function normalizeColorArray(
150
+ value: unknown,
151
+ propPath: string,
152
+ ): string[] | undefined {
153
+ if (value === undefined) return undefined;
154
+ if (!Array.isArray(value)) {
155
+ throw new Error(
156
+ `[USER_ERROR]: <Card>: Invalid prop "${propPath}": expected array (in ${Astro.url.pathname})`,
157
+ );
158
+ }
159
+ if (value.length < 1 || value.length > 4) {
160
+ throw new Error(
161
+ `[USER_ERROR]: <Card>: Invalid prop "${propPath}": expected 1 to 4 colors (in ${Astro.url.pathname})`,
162
+ );
163
+ }
164
+
165
+ return value.map((color, index) =>
166
+ normalizeHexColor(color, `${propPath}.${index}`),
167
+ );
168
+ }
169
+
170
+ function normalizeSeed(value: unknown, propPath: string): string | undefined {
171
+ if (value === undefined) return undefined;
172
+ if (typeof value !== "string") {
173
+ throw new Error(
174
+ `[USER_ERROR]: <Card>: Invalid prop "${propPath}": expected string (in ${Astro.url.pathname})`,
175
+ );
176
+ }
177
+
178
+ const trimmedValue = value.trim();
179
+ if (trimmedValue.length === 0) {
180
+ throw new Error(
181
+ `[USER_ERROR]: <Card>: Invalid prop "${propPath}": expected a non-empty string (in ${Astro.url.pathname})`,
182
+ );
183
+ }
184
+
185
+ return trimmedValue;
186
+ }
187
+
188
+ function normalizeThemeColorConfig(
189
+ value: unknown,
190
+ propPath: string,
191
+ ): string | ThemeColorByMode | undefined {
192
+ if (value === undefined) return undefined;
193
+ if (typeof value === "string") {
194
+ return normalizeHexColor(value, propPath);
195
+ }
196
+
197
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
198
+ throw new Error(
199
+ `[USER_ERROR]: <Card>: Invalid prop "${propPath}": expected a hex color string or an object with light/dark values (in ${Astro.url.pathname})`,
200
+ );
201
+ }
202
+
203
+ const colorByMode = value as Record<string, unknown>;
204
+ for (const key of Object.keys(colorByMode)) {
205
+ if (key !== "light" && key !== "dark") {
206
+ throw new Error(
207
+ `[USER_ERROR]: <Card>: Invalid prop "${propPath}.${key}": only "light" and "dark" are supported (in ${Astro.url.pathname})`,
208
+ );
209
+ }
210
+ }
211
+
212
+ const light =
213
+ colorByMode.light !== undefined
214
+ ? normalizeHexColor(colorByMode.light, `${propPath}.light`)
215
+ : undefined;
216
+ const dark =
217
+ colorByMode.dark !== undefined
218
+ ? normalizeHexColor(colorByMode.dark, `${propPath}.dark`)
219
+ : undefined;
220
+
221
+ if (light === undefined && dark === undefined) {
222
+ throw new Error(
223
+ `[USER_ERROR]: <Card>: Invalid prop "${propPath}": expected at least one of "light" or "dark" (in ${Astro.url.pathname})`,
224
+ );
225
+ }
226
+
227
+ return {
228
+ ...(light !== undefined ? { light } : {}),
229
+ ...(dark !== undefined ? { dark } : {}),
230
+ };
231
+ }
232
+
233
+ function normalizeRequiredString(value: unknown, propPath: string): string {
234
+ if (typeof value !== "string") {
235
+ throw new Error(
236
+ `[USER_ERROR]: <Card>: Invalid prop "${propPath}": expected string (in ${Astro.url.pathname})`,
237
+ );
238
+ }
239
+
240
+ const trimmedValue = value.trim();
241
+ if (trimmedValue.length === 0) {
242
+ throw new Error(
243
+ `[USER_ERROR]: <Card>: Invalid prop "${propPath}": expected a non-empty string (in ${Astro.url.pathname})`,
244
+ );
245
+ }
246
+
247
+ return trimmedValue;
248
+ }
249
+
250
+ function getConfiguredColorForMode(
251
+ value: string | ThemeColorByMode | undefined,
252
+ mode: "light" | "dark",
253
+ ): string | undefined {
254
+ if (typeof value === "string") return value;
255
+ return value?.[mode];
256
+ }
257
+
258
+ const { title, href, icon, cover } = cardProps as Props;
259
+ const resolvedTitle = title.trim();
260
+ const resolvedHref =
261
+ typeof href === "string" && href.trim().length > 0
262
+ ? withBasePath(href)
263
+ : undefined;
264
+ const contentIcon = icon?.trim();
265
+ const coverIcon = cover?.icon?.trim();
266
+ const coverText = cover?.text?.trim();
267
+ const hasCover = Boolean(coverIcon || coverText);
268
+ const cardButton = buttonProps
269
+ ? {
270
+ text: normalizeRequiredString(buttonProps.text, "button.text"),
271
+ href: withBasePath(
272
+ normalizeRequiredString(buttonProps.href, "button.href"),
273
+ ),
274
+ color: normalizeThemeColorConfig(buttonProps.color, "button.color"),
275
+ }
276
+ : undefined;
277
+ const explicitCoverColors = normalizeColorArray(
278
+ coverProps?.colors,
279
+ "cover.colors",
280
+ );
281
+ const explicitPatternSeed = normalizeSeed(
282
+ coverProps?.patternSeed,
283
+ "cover.patternSeed",
284
+ );
285
+ const explicitColorSeed = normalizeSeed(
286
+ coverProps?.colorSeed,
287
+ "cover.colorSeed",
288
+ );
289
+ const config =
290
+ cardButton || (hasCover && (!explicitCoverColors || !explicitColorSeed))
291
+ ? await getConfig()
292
+ : undefined;
293
+ const themeColorCoverColors =
294
+ typeof config?.theme?.themeColor === "string"
295
+ ? [config.theme.themeColor]
296
+ : undefined;
297
+ const coverColors =
298
+ explicitCoverColors ??
299
+ config?.theme?.card?.cover?.colors ??
300
+ themeColorCoverColors;
301
+ const coverColorSeed =
302
+ explicitColorSeed ?? config?.theme?.card?.cover?.colorSeed;
303
+ const useThemeColorForCover =
304
+ coverColors === undefined && config?.theme?.themeColor !== undefined;
305
+ function getDefaultButtonColor(mode: "light" | "dark"): string {
306
+ return getDocsBaseColorShade(
307
+ config?.theme,
308
+ mode,
309
+ mode === "light" ? "900" : "100",
310
+ );
311
+ }
312
+
313
+ function getButtonForegroundColor(
314
+ mode: "light" | "dark",
315
+ color: string,
316
+ ): string {
317
+ return getThemeForegroundColor(
318
+ color,
319
+ getDocsBaseColorShade(config?.theme, mode, "900"),
320
+ );
321
+ }
322
+
323
+ function getNavbarPrimaryThemeColor(mode: "light" | "dark"): string {
324
+ return (
325
+ getConfiguredColorForMode(config?.navbar?.primary?.color, mode) ??
326
+ getDefaultButtonColor(mode)
327
+ );
328
+ }
329
+
330
+ function getCardButtonThemeColor(mode: "light" | "dark"): string {
331
+ return (
332
+ getConfiguredColorForMode(cardButton?.color, mode) ??
333
+ getConfiguredColorForMode(config?.theme?.card?.button?.color, mode) ??
334
+ getNavbarPrimaryThemeColor(mode)
335
+ );
336
+ }
337
+
338
+ const cardButtonThemeColors = cardButton
339
+ ? {
340
+ light: getCardButtonThemeColor("light"),
341
+ dark: getCardButtonThemeColor("dark"),
342
+ }
343
+ : null;
344
+ const cardButtonStyle = cardButtonThemeColors
345
+ ? [
346
+ `--rd-card-button-theme-light: ${cardButtonThemeColors.light}`,
347
+ `--rd-card-button-theme-dark: ${cardButtonThemeColors.dark}`,
348
+ `--rd-card-button-foreground-light: ${getButtonForegroundColor(
349
+ "light",
350
+ cardButtonThemeColors.light,
351
+ )}`,
352
+ `--rd-card-button-foreground-dark: ${getButtonForegroundColor(
353
+ "dark",
354
+ cardButtonThemeColors.dark,
355
+ )}`,
356
+ ].join("; ")
357
+ : "";
358
+ const descriptionHtml = Astro.slots.has("default")
359
+ ? (await Astro.slots.render("default")).trim()
360
+ : "";
361
+ const rootIsLink = Boolean(resolvedHref && !cardButton);
362
+ const Element = rootIsLink ? "a" : "article";
363
+ const rootAttrs = rootIsLink ? { href: resolvedHref } : {};
364
+ const CardGradient = hasCover
365
+ ? (await import("./CardGradient.astro")).default
366
+ : undefined;
367
+ ---
368
+
369
+ <div class="rd-prose-block">
370
+ <Element
371
+ {...rootAttrs}
372
+ data-rd-card-root
373
+ data-rd-card-link={resolvedHref ? "true" : undefined}
374
+ class:list={[
375
+ "not-prose group/rd-card block text-left no-underline transition duration-200 relative z-10 before:absolute before:-z-10 before:rounded-b-xl before:inset-0 before:duration-200",
376
+ resolvedHref &&
377
+ "hover:before:-inset-2.5 hover:before:bg-neutral-50 dark:hover:before:bg-(--rd-code-surface)",
378
+ rootIsLink &&
379
+ "cursor-pointer outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
380
+ hasCover
381
+ ? "before:rounded-t-2xl"
382
+ : "before:rounded-t-xl dark:before:bg-(--rd-code-surface)",
383
+ ]}
384
+ >
385
+ {
386
+ CardGradient && (
387
+ <CardGradient
388
+ title={resolvedTitle}
389
+ icon={coverIcon}
390
+ text={coverText}
391
+ interactive={Boolean(resolvedHref)}
392
+ glass={cover?.glass}
393
+ colors={coverColors}
394
+ patternSeed={explicitPatternSeed}
395
+ colorSeed={coverColorSeed}
396
+ useThemeColor={useThemeColorForCover}
397
+ />
398
+ )
399
+ }
400
+
401
+ <div
402
+ class:list={[
403
+ "flex min-w-0 gap-3",
404
+ cardButton
405
+ ? "flex-col xs:flex-row xs:items-center xs:justify-between"
406
+ : "items-start",
407
+ hasCover && "pt-3 px-0.5",
408
+ ]}
409
+ >
410
+ <div class="flex min-w-0 flex-1 gap-2">
411
+ {
412
+ contentIcon && (
413
+ <span class="self-start flex size-12 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-neutral-900/10 bg-linear-to-br from-white/80 to-neutral-100/60 dark:from-white/7 dark:to-white/2 text-neutral-600 dark:border-white/8 dark:text-neutral-300/90 shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-4px_rgba(0,0,0,0.08)]">
414
+ <Icon name={contentIcon} class="size-5" />
415
+ </span>
416
+ )
417
+ }
418
+ <div class="ml-1 self-center min-w-0 flex-1">
419
+ <h4
420
+ class="not-prose m-0 flex min-w-0 items-center text-base font-semibold text-neutral-900 dark:text-neutral-100"
421
+ >
422
+ {
423
+ resolvedHref && cardButton ? (
424
+ <a
425
+ href={resolvedHref}
426
+ class="group/rd-card-title inline-flex min-w-0 items-center rounded-sm text-inherit no-underline outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]"
427
+ >
428
+ <span class="min-w-0 leading-6">{resolvedTitle}</span>
429
+ <Icon
430
+ name="lucide:chevron-right"
431
+ class="ml-px mt-px size-4 shrink-0 -translate-x-1 opacity-0 transition duration-200 ease-out group-hover/rd-card:translate-x-0 group-hover/rd-card:opacity-90 group-focus-visible/rd-card-title:translate-x-0 group-focus-visible/rd-card-title:opacity-100"
432
+ />
433
+ </a>
434
+ ) : (
435
+ <>
436
+ <span class="min-w-0 leading-6">{resolvedTitle}</span>
437
+ {resolvedHref && (
438
+ <Icon
439
+ name="lucide:chevron-right"
440
+ class="ml-px mt-px size-4 shrink-0 -translate-x-1 opacity-0 transition duration-200 ease-out group-hover/rd-card:translate-x-0 group-hover/rd-card:opacity-90 group-focus-visible/rd-card:translate-x-0 group-focus-visible/rd-card:opacity-100"
441
+ />
442
+ )}
443
+ </>
444
+ )
445
+ }
446
+ </h4>
447
+ {
448
+ descriptionHtml && (
449
+ <div
450
+ class="prose-rules prose-sm! leading-6 text-sm! text-neutral-500 **:text-neutral-500 dark:text-neutral-300 dark:**:text-neutral-300/75"
451
+ set:html={descriptionHtml}
452
+ />
453
+ )
454
+ }
455
+ </div>
456
+ </div>
457
+ {
458
+ cardButton && (
459
+ <a
460
+ href={cardButton.href}
461
+ class="rd-card-button relative z-10 inline-flex h-8 shrink-0 items-center justify-center self-center rounded-xl [corner-shape:superellipse(1.2)] px-3 text-[13px] font-[350] no-underline transition-opacity duration-200 hover:opacity-95 focus-visible:outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] dark:font-[450]"
462
+ style={cardButtonStyle}
463
+ >
464
+ {cardButton.text}
465
+ </a>
466
+ )
467
+ }
468
+ </div>
469
+ </Element>
470
+ </div>
471
+
472
+ <style>
473
+ .rd-card-button {
474
+ --rd-card-button-theme: var(--rd-card-button-theme-light);
475
+ --rd-card-button-foreground: var(--rd-card-button-foreground-light);
476
+ background: linear-gradient(
477
+ to bottom,
478
+ color-mix(in oklab, var(--rd-card-button-theme) 88%, white),
479
+ color-mix(in oklab, var(--rd-card-button-theme) 90%, black)
480
+ );
481
+ color: var(--rd-card-button-foreground);
482
+ }
483
+
484
+ :global(.dark) .rd-card-button {
485
+ --rd-card-button-theme: var(--rd-card-button-theme-dark);
486
+ --rd-card-button-foreground: var(--rd-card-button-foreground-dark);
487
+ }
488
+ </style>