radiant-docs 0.1.41 → 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 (32) 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 +1 -0
  5. package/template/src/components/Header.astro +150 -16
  6. package/template/src/components/MdxPage.astro +76 -22
  7. package/template/src/components/PagePagination.astro +44 -8
  8. package/template/src/components/Sidebar.astro +10 -1
  9. package/template/src/components/TableOfContents.astro +159 -53
  10. package/template/src/components/chat/AssistantDocsWidget.tsx +221 -8
  11. package/template/src/components/chat/AssistantEmbedPanel.tsx +1090 -104
  12. package/template/src/components/user/Accordion.astro +2 -2
  13. package/template/src/components/user/AccordionGroup.astro +1 -1
  14. package/template/src/components/user/Callout.astro +2 -2
  15. package/template/src/components/user/Card.astro +488 -0
  16. package/template/src/components/user/CardGradient.astro +964 -0
  17. package/template/src/components/user/CodeBlock.astro +1 -1
  18. package/template/src/components/user/CodeGroup.astro +1 -1
  19. package/template/src/components/user/Column.astro +25 -0
  20. package/template/src/components/user/Columns.astro +200 -0
  21. package/template/src/components/user/ComponentPreviewBlock.astro +1 -1
  22. package/template/src/components/user/Image.astro +1 -1
  23. package/template/src/components/user/Step.astro +1 -1
  24. package/template/src/components/user/Steps.astro +1 -1
  25. package/template/src/components/user/Tab.astro +1 -3
  26. package/template/src/components/user/Tabs.astro +2 -2
  27. package/template/src/layouts/Layout.astro +2 -4
  28. package/template/src/lib/assistant-chrome-defaults.ts +12 -0
  29. package/template/src/lib/assistant-embed-script.ts +209 -18
  30. package/template/src/lib/validation.ts +325 -75
  31. package/template/src/styles/global.css +81 -4
  32. package/template/src/components/chat/AskAiWidget.tsx +0 -2011
@@ -0,0 +1,964 @@
1
+ ---
2
+ import Icon from "../ui/Icon.astro";
3
+
4
+ interface Props {
5
+ title: string;
6
+ icon?: string;
7
+ text?: string;
8
+ interactive?: boolean;
9
+ glass?: boolean;
10
+ colors?: string[];
11
+ patternSeed?: string;
12
+ colorSeed?: string;
13
+ useThemeColor?: boolean;
14
+ }
15
+
16
+ const {
17
+ title,
18
+ icon,
19
+ text,
20
+ interactive = false,
21
+ glass = false,
22
+ colors,
23
+ patternSeed,
24
+ colorSeed,
25
+ useThemeColor = false,
26
+ } = Astro.props as Props;
27
+
28
+ const DEFAULT_COVER_COLORS = ["#fb7185", "#fb923c", "#fcd34d", "#f9a8d4"];
29
+
30
+ function hashString(value: string): number {
31
+ let hash = 2166136261;
32
+ for (let index = 0; index < value.length; index += 1) {
33
+ hash ^= value.charCodeAt(index);
34
+ hash = Math.imul(hash, 16777619);
35
+ }
36
+ return hash >>> 0;
37
+ }
38
+
39
+ function seededRatio(seed: number, salt: number): number {
40
+ let value = seed ^ Math.imul(salt, 0x9e3779b1);
41
+ value ^= value >>> 16;
42
+ value = Math.imul(value, 0x85ebca6b);
43
+ value ^= value >>> 13;
44
+ value = Math.imul(value, 0xc2b2ae35);
45
+ value ^= value >>> 16;
46
+ return (value >>> 0) / 4294967295;
47
+ }
48
+
49
+ function range(seed: number, salt: number, min: number, max: number): number {
50
+ return min + (max - min) * seededRatio(seed, salt);
51
+ }
52
+
53
+ function clamp(value: number, min: number, max: number): number {
54
+ return Math.min(Math.max(value, min), max);
55
+ }
56
+
57
+ function hue(value: number): number {
58
+ return ((value % 360) + 360) % 360;
59
+ }
60
+
61
+ function parseRgb(value: string): { r: number; g: number; b: number } | null {
62
+ const hex = value
63
+ .trim()
64
+ .match(/^#([a-f\d]{3}|[a-f\d]{4}|[a-f\d]{6}|[a-f\d]{8})$/i);
65
+ if (hex?.[1]) {
66
+ const raw =
67
+ hex[1].length === 3 || hex[1].length === 4
68
+ ? hex[1]
69
+ .slice(0, 3)
70
+ .split("")
71
+ .map((part) => `${part}${part}`)
72
+ .join("")
73
+ : hex[1].slice(0, 6);
74
+ return {
75
+ r: Number.parseInt(raw.slice(0, 2), 16),
76
+ g: Number.parseInt(raw.slice(2, 4), 16),
77
+ b: Number.parseInt(raw.slice(4, 6), 16),
78
+ };
79
+ }
80
+
81
+ return null;
82
+ }
83
+
84
+ function rgbToHsl({ r, g, b }: { r: number; g: number; b: number }): {
85
+ h: number;
86
+ s: number;
87
+ l: number;
88
+ } {
89
+ const red = r / 255;
90
+ const green = g / 255;
91
+ const blue = b / 255;
92
+ const max = Math.max(red, green, blue);
93
+ const min = Math.min(red, green, blue);
94
+ const lightness = (max + min) / 2;
95
+ const delta = max - min;
96
+
97
+ if (delta === 0) return { h: 0, s: 0, l: lightness * 100 };
98
+
99
+ const saturation = delta / (1 - Math.abs(2 * lightness - 1));
100
+ let hueValue = 0;
101
+
102
+ if (max === red) {
103
+ hueValue = ((green - blue) / delta) % 6;
104
+ } else if (max === green) {
105
+ hueValue = (blue - red) / delta + 2;
106
+ } else {
107
+ hueValue = (red - green) / delta + 4;
108
+ }
109
+
110
+ return {
111
+ h: hue(hueValue * 60),
112
+ s: saturation * 100,
113
+ l: lightness * 100,
114
+ };
115
+ }
116
+
117
+ function hslColor(h: number, s: number, l: number): string {
118
+ return `hsl(${Math.round(hue(h))}, ${Math.round(
119
+ clamp(s, 0, 100),
120
+ )}%, ${Math.round(clamp(l, 0, 100))}%)`;
121
+ }
122
+
123
+ function hueDistance(first: number, second: number): number {
124
+ const distance = Math.abs(hue(first) - hue(second));
125
+ return Math.min(distance, 360 - distance);
126
+ }
127
+
128
+ type PaletteRecipe = {
129
+ offsets: [number, number, number];
130
+ jitter: number;
131
+ saturation: [number, number, number];
132
+ lightness: [number, number, number];
133
+ };
134
+
135
+ const DEFAULT_PALETTE_RECIPE: PaletteRecipe = {
136
+ offsets: [-32, 28, 54],
137
+ jitter: 14,
138
+ saturation: [1.02, 0.96, 0.92],
139
+ lightness: [8, 0, 10],
140
+ };
141
+
142
+ const SEEDED_PALETTE_RECIPES: PaletteRecipe[] = [
143
+ DEFAULT_PALETTE_RECIPE,
144
+ {
145
+ offsets: [142, 190, -26],
146
+ jitter: 22,
147
+ saturation: [0.88, 0.8, 1.04],
148
+ lightness: [10, 14, 4],
149
+ },
150
+ {
151
+ offsets: [118, 238, 34],
152
+ jitter: 18,
153
+ saturation: [0.9, 0.86, 1.06],
154
+ lightness: [9, 8, 1],
155
+ },
156
+ {
157
+ offsets: [20, 46, -20],
158
+ jitter: 20,
159
+ saturation: [1.08, 0.98, 1.04],
160
+ lightness: [5, 12, 2],
161
+ },
162
+ {
163
+ offsets: [164, 210, 30],
164
+ jitter: 20,
165
+ saturation: [0.86, 0.78, 1.02],
166
+ lightness: [12, 16, 3],
167
+ },
168
+ {
169
+ offsets: [176, 32, 220],
170
+ jitter: 26,
171
+ saturation: [0.64, 0.72, 0.62],
172
+ lightness: [18, 14, 20],
173
+ },
174
+ {
175
+ offsets: [88, 178, 274],
176
+ jitter: 24,
177
+ saturation: [1.16, 1.04, 1.08],
178
+ lightness: [2, 8, 4],
179
+ },
180
+ ];
181
+
182
+ function getPaletteRecipe(seed: number, hasSeedValue: boolean): PaletteRecipe {
183
+ if (!hasSeedValue) return DEFAULT_PALETTE_RECIPE;
184
+ return SEEDED_PALETTE_RECIPES[seed % SEEDED_PALETTE_RECIPES.length];
185
+ }
186
+
187
+ function getGeneratedPalette(seedSource: string, baseColor: string): string[] {
188
+ const baseRgb = parseRgb(baseColor) ?? { r: 31, g: 111, b: 235 };
189
+ const preferred = rgbToHsl(baseRgb);
190
+ const isNeutralPreference =
191
+ preferred.s < 12 || preferred.l < 16 || preferred.l > 88;
192
+ const colorSeed = hashString(seedSource);
193
+ const hasSeedValue = seedSource.includes("::");
194
+ const recipe = getPaletteRecipe(colorSeed, hasSeedValue);
195
+ const baseHue = isNeutralPreference
196
+ ? range(colorSeed, 20, 8, 348)
197
+ : preferred.h;
198
+ const baseSaturation = isNeutralPreference
199
+ ? range(colorSeed, 21, 62, 78)
200
+ : preferred.s;
201
+ const baseLightness = isNeutralPreference
202
+ ? range(colorSeed, 22, 56, 68)
203
+ : preferred.l;
204
+ const usedHues = [baseHue];
205
+
206
+ return recipe.offsets.map((offset, index) => {
207
+ let colorHue =
208
+ baseHue +
209
+ offset +
210
+ range(colorSeed, 30 + index, -recipe.jitter, recipe.jitter);
211
+
212
+ for (let attempt = 0; attempt < 3; attempt += 1) {
213
+ if (usedHues.every((usedHue) => hueDistance(colorHue, usedHue) >= 18)) {
214
+ break;
215
+ }
216
+ colorHue += range(colorSeed, 40 + index + attempt, 28, 58);
217
+ }
218
+
219
+ usedHues.push(colorHue);
220
+ return hslColor(
221
+ colorHue,
222
+ baseSaturation * recipe.saturation[index] +
223
+ range(colorSeed, 50 + index, -9, 9),
224
+ 62 +
225
+ (baseLightness - 50) * 0.18 +
226
+ recipe.lightness[index] +
227
+ range(colorSeed, 60 + index, -6, 6),
228
+ );
229
+ });
230
+ }
231
+
232
+ function completePalette(
233
+ inputColors?: string[],
234
+ seedValue?: string,
235
+ ): string[] | undefined {
236
+ const palette = inputColors
237
+ ?.map((color) => color.trim())
238
+ .filter(Boolean)
239
+ .slice(0, 4);
240
+ if (!palette?.length) return undefined;
241
+ if (palette.length === 4) return palette;
242
+
243
+ const seedSource = seedValue
244
+ ? `${palette.join("|")}::${seedValue}`
245
+ : palette.join("|");
246
+ const generated = getGeneratedPalette(seedSource, palette[0]);
247
+ for (const color of generated) {
248
+ if (palette.length === 4) break;
249
+ palette.push(color);
250
+ }
251
+
252
+ return palette;
253
+ }
254
+
255
+ const patternSeedSource = patternSeed ? `${title}::${patternSeed}` : title;
256
+ const seed = hashString(patternSeedSource);
257
+ const explicitPalette = completePalette(colors, colorSeed);
258
+ const defaultPalette =
259
+ colorSeed !== undefined
260
+ ? completePalette([DEFAULT_COVER_COLORS[0]], colorSeed)
261
+ : DEFAULT_COVER_COLORS;
262
+ const staticPalette =
263
+ explicitPalette ?? (useThemeColor ? undefined : defaultPalette);
264
+ const gradientColors = staticPalette ?? [
265
+ "color-mix(in oklab, var(--color-theme) 62%, #fda4af)",
266
+ "color-mix(in oklab, var(--color-theme) 48%, #93c5fd)",
267
+ "color-mix(in oklab, var(--color-theme) 56%, #f0abfc)",
268
+ "color-mix(in oklab, var(--color-theme) 58%, #60a5fa)",
269
+ ];
270
+ const gradientStyle = [
271
+ `--rd-card-gradient-angle: ${Math.round(range(seed, 1, 112, 154))}deg`,
272
+ `--rd-card-gradient-x: ${Math.round(range(seed, 2, 12, 88))}%`,
273
+ `--rd-card-gradient-y: ${Math.round(range(seed, 3, 4, 44))}%`,
274
+ `--rd-card-gradient-x-2: ${Math.round(range(seed, 4, 32, 92))}%`,
275
+ `--rd-card-gradient-y-2: ${Math.round(range(seed, 5, 54, 96))}%`,
276
+ `--rd-card-gradient-color-1: ${gradientColors[0]}`,
277
+ `--rd-card-gradient-color-2: ${gradientColors[1]}`,
278
+ `--rd-card-gradient-color-3: ${gradientColors[2]}`,
279
+ `--rd-card-gradient-color-4: ${gradientColors[3]}`,
280
+ ].join("; ");
281
+ const gradientColorData = staticPalette
282
+ ? JSON.stringify(staticPalette)
283
+ : undefined;
284
+ const isIconOnly = Boolean(icon && !text);
285
+ const textSizeClass = icon ? "text-lg sm:text-xl" : "text-xl sm:text-2xl";
286
+ ---
287
+
288
+ <div
289
+ class="rd-card-gradient-frame relative isolate flex min-h-44 w-full items-center justify-center overflow-hidden rounded-xl bg-transparent"
290
+ >
291
+ <div
292
+ class="rd-card-gradient-scale absolute inset-0 flex items-center justify-center"
293
+ >
294
+ <div
295
+ data-rd-card-gradient
296
+ data-rd-card-motion={interactive ? "true" : "false"}
297
+ data-rd-card-seed={patternSeedSource}
298
+ data-rd-card-color-seed={colorSeed}
299
+ data-rd-card-colors={gradientColorData}
300
+ style={gradientStyle}
301
+ class="rd-card-gradient absolute inset-0 isolate overflow-hidden bg-transparent"
302
+ >
303
+ <div class="rd-card-gradient-fallback absolute inset-0 -z-20"></div>
304
+ <div
305
+ class="pointer-events-none absolute inset-0 z-0 bg-[linear-gradient(120deg,rgba(255,255,255,0.36),rgba(255,255,255,0.06)_38%,rgba(255,255,255,0.18)_72%,rgba(255,255,255,0.04))]"
306
+ >
307
+ </div>
308
+ </div>
309
+ <div
310
+ class:list={[
311
+ "rd-card-gradient-content relative z-10 flex flex-col items-center justify-center gap-2 px-5 text-center text-white drop-shadow-[0_1px_10px_rgba(0,0,0,0.18)]",
312
+ glass && "rd-card-gradient-content-glass rounded-2xl",
313
+ glass && isIconOnly && "rd-card-gradient-content-glass-icon",
314
+ ]}
315
+ >
316
+ {icon && <Icon name={icon} class="size-9 text-white/70" />}
317
+ {
318
+ text && (
319
+ <span
320
+ class:list={[
321
+ "max-w-full wrap-break-word font-semibold leading-tight text-white/70",
322
+ textSizeClass,
323
+ ]}
324
+ >
325
+ {text}
326
+ </span>
327
+ )
328
+ }
329
+ </div>
330
+ </div>
331
+ </div>
332
+
333
+ <script>
334
+ type ShaderModule = typeof import("@paper-design/shaders");
335
+ type ShaderMountInstance = InstanceType<ShaderModule["ShaderMount"]>;
336
+ type GradientState = {
337
+ mount: ShaderMountInstance;
338
+ hoverSpeed: number;
339
+ motionBound: boolean;
340
+ };
341
+ type CardGradientWindow = Window & {
342
+ __rdCardGradientListenersBound?: boolean;
343
+ __rdCardGradientPending?: WeakSet<HTMLElement>;
344
+ __rdCardGradientRegistry?: WeakMap<HTMLElement, GradientState>;
345
+ __rdCardShaderModulePromise?: Promise<ShaderModule>;
346
+ };
347
+
348
+ const cardWindow = window as CardGradientWindow;
349
+ const registry = (cardWindow.__rdCardGradientRegistry ??= new WeakMap());
350
+ const pending = (cardWindow.__rdCardGradientPending ??= new WeakSet());
351
+ const CARD_GRADIENT_GRAIN_MIXER = 0.075;
352
+ const CARD_GRADIENT_GRAIN_OVERLAY = 0.055;
353
+ const CARD_GRADIENT_HOVER_SPEED = 0.32;
354
+
355
+ function loadShaders(): Promise<ShaderModule> {
356
+ return (cardWindow.__rdCardShaderModulePromise ??=
357
+ import("@paper-design/shaders"));
358
+ }
359
+
360
+ function hashString(value: string): number {
361
+ let hash = 2166136261;
362
+ for (let index = 0; index < value.length; index += 1) {
363
+ hash ^= value.charCodeAt(index);
364
+ hash = Math.imul(hash, 16777619);
365
+ }
366
+ return hash >>> 0;
367
+ }
368
+
369
+ function seededRatio(seed: number, salt: number): number {
370
+ let value = seed ^ Math.imul(salt, 0x9e3779b1);
371
+ value ^= value >>> 16;
372
+ value = Math.imul(value, 0x85ebca6b);
373
+ value ^= value >>> 13;
374
+ value = Math.imul(value, 0xc2b2ae35);
375
+ value ^= value >>> 16;
376
+ return (value >>> 0) / 4294967295;
377
+ }
378
+
379
+ function range(seed: number, salt: number, min: number, max: number): number {
380
+ return min + (max - min) * seededRatio(seed, salt);
381
+ }
382
+
383
+ function clamp(value: number, min: number, max: number): number {
384
+ return Math.min(Math.max(value, min), max);
385
+ }
386
+
387
+ function hue(value: number): number {
388
+ return ((value % 360) + 360) % 360;
389
+ }
390
+
391
+ function parseRgb(value: string): { r: number; g: number; b: number } | null {
392
+ const hex = value
393
+ .trim()
394
+ .match(/^#([a-f\d]{3}|[a-f\d]{4}|[a-f\d]{6}|[a-f\d]{8})$/i);
395
+ if (hex?.[1]) {
396
+ const raw =
397
+ hex[1].length === 3 || hex[1].length === 4
398
+ ? hex[1]
399
+ .slice(0, 3)
400
+ .split("")
401
+ .map((part) => `${part}${part}`)
402
+ .join("")
403
+ : hex[1].slice(0, 6);
404
+ return {
405
+ r: Number.parseInt(raw.slice(0, 2), 16),
406
+ g: Number.parseInt(raw.slice(2, 4), 16),
407
+ b: Number.parseInt(raw.slice(4, 6), 16),
408
+ };
409
+ }
410
+
411
+ const rgb = value
412
+ .trim()
413
+ .match(/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/i);
414
+ if (!rgb) return null;
415
+
416
+ return {
417
+ r: Number.parseFloat(rgb[1] ?? "0"),
418
+ g: Number.parseFloat(rgb[2] ?? "0"),
419
+ b: Number.parseFloat(rgb[3] ?? "0"),
420
+ };
421
+ }
422
+
423
+ function rgbToHsl({ r, g, b }: { r: number; g: number; b: number }): {
424
+ h: number;
425
+ s: number;
426
+ l: number;
427
+ } {
428
+ const red = r / 255;
429
+ const green = g / 255;
430
+ const blue = b / 255;
431
+ const max = Math.max(red, green, blue);
432
+ const min = Math.min(red, green, blue);
433
+ const lightness = (max + min) / 2;
434
+ const delta = max - min;
435
+
436
+ if (delta === 0) return { h: 0, s: 0, l: lightness * 100 };
437
+
438
+ const saturation = delta / (1 - Math.abs(2 * lightness - 1));
439
+ let hueValue = 0;
440
+
441
+ if (max === red) {
442
+ hueValue = ((green - blue) / delta) % 6;
443
+ } else if (max === green) {
444
+ hueValue = (blue - red) / delta + 2;
445
+ } else {
446
+ hueValue = (red - green) / delta + 4;
447
+ }
448
+
449
+ return {
450
+ h: hue(hueValue * 60),
451
+ s: saturation * 100,
452
+ l: lightness * 100,
453
+ };
454
+ }
455
+
456
+ function hslColor(h: number, s: number, l: number): string {
457
+ return `hsl(${Math.round(hue(h))}, ${Math.round(
458
+ clamp(s, 0, 100),
459
+ )}%, ${Math.round(clamp(l, 0, 100))}%)`;
460
+ }
461
+
462
+ function hueDistance(first: number, second: number): number {
463
+ const distance = Math.abs(hue(first) - hue(second));
464
+ return Math.min(distance, 360 - distance);
465
+ }
466
+
467
+ type PaletteRecipe = {
468
+ offsets: [number, number, number];
469
+ jitter: number;
470
+ saturation: [number, number, number];
471
+ lightness: [number, number, number];
472
+ };
473
+
474
+ const DEFAULT_PALETTE_RECIPE: PaletteRecipe = {
475
+ offsets: [-32, 28, 54],
476
+ jitter: 14,
477
+ saturation: [1.02, 0.96, 0.92],
478
+ lightness: [8, 0, 10],
479
+ };
480
+
481
+ const SEEDED_PALETTE_RECIPES: PaletteRecipe[] = [
482
+ DEFAULT_PALETTE_RECIPE,
483
+ {
484
+ offsets: [142, 190, -26],
485
+ jitter: 22,
486
+ saturation: [0.88, 0.8, 1.04],
487
+ lightness: [10, 14, 4],
488
+ },
489
+ {
490
+ offsets: [118, 238, 34],
491
+ jitter: 18,
492
+ saturation: [0.9, 0.86, 1.06],
493
+ lightness: [9, 8, 1],
494
+ },
495
+ {
496
+ offsets: [20, 46, -20],
497
+ jitter: 20,
498
+ saturation: [1.08, 0.98, 1.04],
499
+ lightness: [5, 12, 2],
500
+ },
501
+ {
502
+ offsets: [164, 210, 30],
503
+ jitter: 20,
504
+ saturation: [0.86, 0.78, 1.02],
505
+ lightness: [12, 16, 3],
506
+ },
507
+ {
508
+ offsets: [176, 32, 220],
509
+ jitter: 26,
510
+ saturation: [0.64, 0.72, 0.62],
511
+ lightness: [18, 14, 20],
512
+ },
513
+ {
514
+ offsets: [88, 178, 274],
515
+ jitter: 24,
516
+ saturation: [1.16, 1.04, 1.08],
517
+ lightness: [2, 8, 4],
518
+ },
519
+ ];
520
+
521
+ function getPaletteRecipe(
522
+ seed: number,
523
+ hasSeedValue: boolean,
524
+ ): PaletteRecipe {
525
+ if (!hasSeedValue) return DEFAULT_PALETTE_RECIPE;
526
+ return SEEDED_PALETTE_RECIPES[seed % SEEDED_PALETTE_RECIPES.length];
527
+ }
528
+
529
+ function getGeneratedPalette(
530
+ seedSource: string,
531
+ baseColor: string,
532
+ ): string[] {
533
+ const baseRgb = parseRgb(baseColor) ?? { r: 31, g: 111, b: 235 };
534
+ const preferred = rgbToHsl(baseRgb);
535
+ const isNeutralPreference =
536
+ preferred.s < 12 || preferred.l < 16 || preferred.l > 88;
537
+ const colorSeed = hashString(seedSource);
538
+ const hasSeedValue = seedSource.includes("::");
539
+ const recipe = getPaletteRecipe(colorSeed, hasSeedValue);
540
+ const baseHue = isNeutralPreference
541
+ ? range(colorSeed, 20, 8, 348)
542
+ : preferred.h;
543
+ const baseSaturation = isNeutralPreference
544
+ ? range(colorSeed, 21, 62, 78)
545
+ : preferred.s;
546
+ const baseLightness = isNeutralPreference
547
+ ? range(colorSeed, 22, 56, 68)
548
+ : preferred.l;
549
+ const usedHues = [baseHue];
550
+
551
+ return recipe.offsets.map((offset, index) => {
552
+ let colorHue =
553
+ baseHue +
554
+ offset +
555
+ range(colorSeed, 30 + index, -recipe.jitter, recipe.jitter);
556
+
557
+ for (let attempt = 0; attempt < 3; attempt += 1) {
558
+ if (usedHues.every((usedHue) => hueDistance(colorHue, usedHue) >= 18)) {
559
+ break;
560
+ }
561
+ colorHue += range(colorSeed, 40 + index + attempt, 28, 58);
562
+ }
563
+
564
+ usedHues.push(colorHue);
565
+ return hslColor(
566
+ colorHue,
567
+ baseSaturation * recipe.saturation[index] +
568
+ range(colorSeed, 50 + index, -9, 9),
569
+ 62 +
570
+ (baseLightness - 50) * 0.18 +
571
+ recipe.lightness[index] +
572
+ range(colorSeed, 60 + index, -6, 6),
573
+ );
574
+ });
575
+ }
576
+
577
+ function completePalette(
578
+ inputColors: string[],
579
+ seedValue?: string,
580
+ ): string[] | null {
581
+ const palette = inputColors
582
+ .map((color) => color.trim())
583
+ .filter(Boolean)
584
+ .slice(0, 4);
585
+ if (palette.length === 0) return null;
586
+ if (palette.length === 4) return palette;
587
+
588
+ const seedSource = seedValue
589
+ ? `${palette.join("|")}::${seedValue}`
590
+ : palette.join("|");
591
+ const generated = getGeneratedPalette(seedSource, palette[0] ?? "");
592
+ for (const color of generated) {
593
+ if (palette.length === 4) break;
594
+ palette.push(color);
595
+ }
596
+
597
+ return palette;
598
+ }
599
+
600
+ function getElementPalette(element: HTMLElement): string[] | null {
601
+ const rawColors = element.dataset.rdCardColors;
602
+ if (!rawColors) return null;
603
+
604
+ try {
605
+ const parsedColors = JSON.parse(rawColors);
606
+ if (
607
+ Array.isArray(parsedColors) &&
608
+ parsedColors.every((color) => typeof color === "string")
609
+ ) {
610
+ return completePalette(parsedColors, element.dataset.rdCardColorSeed);
611
+ }
612
+ } catch {
613
+ return null;
614
+ }
615
+
616
+ return null;
617
+ }
618
+
619
+ function getThemeRgb(): { r: number; g: number; b: number } {
620
+ const themeColor = getComputedStyle(document.documentElement)
621
+ .getPropertyValue("--color-theme")
622
+ .trim();
623
+ return parseRgb(themeColor) ?? { r: 31, g: 111, b: 235 };
624
+ }
625
+
626
+ function rgbToHex({ r, g, b }: { r: number; g: number; b: number }): string {
627
+ const toHex = (channel: number) =>
628
+ Math.round(clamp(channel, 0, 255))
629
+ .toString(16)
630
+ .padStart(2, "0");
631
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
632
+ }
633
+
634
+ function getThemePalette(element: HTMLElement): string[] {
635
+ const themeRgb = getThemeRgb();
636
+ const seedSource = element.dataset.rdCardColorSeed?.trim();
637
+ return (
638
+ completePalette([rgbToHex(themeRgb)], seedSource) ?? [
639
+ "#fb7185",
640
+ "#fb923c",
641
+ "#fcd34d",
642
+ "#f9a8d4",
643
+ ]
644
+ );
645
+ }
646
+
647
+ function getPalette(element: HTMLElement): string[] {
648
+ return getElementPalette(element) ?? getThemePalette(element);
649
+ }
650
+
651
+ function getUniforms(element: HTMLElement, shaders: ShaderModule) {
652
+ const seed = hashString(element.dataset.rdCardSeed ?? "");
653
+ const colors = getPalette(element);
654
+
655
+ return {
656
+ u_colors: colors.map(shaders.getShaderColorFromString),
657
+ u_colorsCount: colors.length,
658
+ u_distortion: range(seed, 30, 0.68, 0.86),
659
+ u_swirl: range(seed, 31, 0.08, 0.2),
660
+ u_grainMixer: CARD_GRADIENT_GRAIN_MIXER,
661
+ u_grainOverlay: CARD_GRADIENT_GRAIN_OVERLAY,
662
+ u_fit: shaders.ShaderFitOptions.cover,
663
+ u_scale: range(seed, 34, 1.08, 1.2),
664
+ u_rotation: range(seed, 35, -8, 8),
665
+ u_offsetX: range(seed, 36, -0.08, 0.08),
666
+ u_offsetY: range(seed, 37, -0.06, 0.06),
667
+ u_originX: 0.5,
668
+ u_originY: 0.5,
669
+ u_worldWidth: 0,
670
+ u_worldHeight: 0,
671
+ };
672
+ }
673
+
674
+ function bindMotion(element: HTMLElement, state: GradientState): void {
675
+ if (state.motionBound || element.dataset.rdCardMotion !== "true") return;
676
+
677
+ const root = element.closest<HTMLElement>("[data-rd-card-root]");
678
+ if (!root) return;
679
+
680
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
681
+ const start = () => {
682
+ if (!reducedMotion.matches) state.mount.setSpeed(state.hoverSpeed);
683
+ };
684
+ const stop = () => state.mount.setSpeed(0);
685
+
686
+ root.addEventListener("pointerenter", start);
687
+ root.addEventListener("pointerleave", stop);
688
+ root.addEventListener("focusin", start);
689
+ root.addEventListener("focusout", (event) => {
690
+ if (
691
+ event.relatedTarget instanceof Node &&
692
+ root.contains(event.relatedTarget)
693
+ ) {
694
+ return;
695
+ }
696
+ stop();
697
+ });
698
+
699
+ state.motionBound = true;
700
+ }
701
+
702
+ async function mountGradient(element: HTMLElement): Promise<void> {
703
+ const existing = registry.get(element);
704
+ if (existing) {
705
+ const shaders = await loadShaders();
706
+ existing.mount.setUniforms(getUniforms(element, shaders));
707
+ element.dataset.rdCardShaderReady = "true";
708
+ bindMotion(element, existing);
709
+ return;
710
+ }
711
+
712
+ if (pending.has(element)) return;
713
+ pending.add(element);
714
+
715
+ try {
716
+ const shaders = await loadShaders();
717
+ if (registry.has(element)) return;
718
+
719
+ const seed = hashString(element.dataset.rdCardSeed ?? "");
720
+ const mount = new shaders.ShaderMount(
721
+ element,
722
+ shaders.meshGradientFragmentShader,
723
+ getUniforms(element, shaders),
724
+ { alpha: true, antialias: true },
725
+ 0,
726
+ Math.round(range(seed, 40, 4000, 32000)),
727
+ 1.35,
728
+ 900000,
729
+ );
730
+ requestAnimationFrame(() => {
731
+ requestAnimationFrame(() => {
732
+ if (registry.has(element)) {
733
+ element.dataset.rdCardShaderReady = "true";
734
+ }
735
+ });
736
+ });
737
+ const state = {
738
+ mount,
739
+ hoverSpeed: CARD_GRADIENT_HOVER_SPEED,
740
+ motionBound: false,
741
+ };
742
+ registry.set(element, state);
743
+ bindMotion(element, state);
744
+ } catch {
745
+ element.dataset.rdCardShaderUnavailable = "true";
746
+ } finally {
747
+ pending.delete(element);
748
+ }
749
+ }
750
+
751
+ function initCardGradients(): void {
752
+ const elements = document.querySelectorAll<HTMLElement>(
753
+ "[data-rd-card-gradient]",
754
+ );
755
+ if (elements.length === 0) return;
756
+ elements.forEach((element) => {
757
+ void mountGradient(element);
758
+ });
759
+ }
760
+
761
+ initCardGradients();
762
+
763
+ if (!cardWindow.__rdCardGradientListenersBound) {
764
+ document.addEventListener("astro:page-load", initCardGradients);
765
+ document.addEventListener("astro:after-swap", initCardGradients);
766
+
767
+ const observer = new MutationObserver(initCardGradients);
768
+ observer.observe(document.documentElement, {
769
+ attributes: true,
770
+ attributeFilter: ["class", "data-theme", "style"],
771
+ });
772
+
773
+ cardWindow.__rdCardGradientListenersBound = true;
774
+ }
775
+ </script>
776
+
777
+ <style>
778
+ .rd-card-gradient-frame::after {
779
+ content: "";
780
+ position: absolute;
781
+ inset: 0;
782
+ z-index: 30;
783
+ pointer-events: none;
784
+ border-radius: inherit;
785
+ box-shadow: inset 0 0 0 1px rgb(255 255 255 / 30%);
786
+ }
787
+
788
+ .rd-card-gradient :global(canvas) {
789
+ opacity: 0;
790
+ filter: blur(8px) saturate(0.94) brightness(1.02);
791
+ transform: scale(1.018);
792
+ transform-origin: center;
793
+ transition:
794
+ opacity 2200ms cubic-bezier(0.22, 1, 0.36, 1),
795
+ filter 2200ms cubic-bezier(0.22, 1, 0.36, 1),
796
+ transform 2400ms cubic-bezier(0.22, 1, 0.36, 1);
797
+ will-change: opacity, filter, transform;
798
+ }
799
+
800
+ .rd-card-gradient[data-rd-card-shader-ready="true"] :global(canvas) {
801
+ opacity: 0.92;
802
+ filter: blur(0) saturate(1) brightness(1);
803
+ transform: scale(1);
804
+ }
805
+
806
+ .rd-card-gradient-content {
807
+ max-width: calc(100% - 2rem);
808
+ }
809
+
810
+ .rd-card-gradient-content-glass {
811
+ isolation: isolate;
812
+ min-width: min(8.5rem, calc(100% - 2rem));
813
+ padding: 0.85rem 1.25rem;
814
+ overflow: hidden;
815
+ border: 1px solid rgb(255 255 255 / 22%);
816
+ background: linear-gradient(
817
+ 180deg,
818
+ rgb(255 255 255 / 18%),
819
+ rgb(255 255 255 / 10%)
820
+ );
821
+ box-shadow:
822
+ inset 0 1px 0 rgb(255 255 255 / 20%),
823
+ inset 0 0 0 1px rgb(255 255 255 / 6%),
824
+ 0 10px 26px rgb(0 0 0 / 5%);
825
+ filter: drop-shadow(0 1px 6px rgb(255 255 255 / 10%));
826
+ backdrop-filter: blur(10px) saturate(1.1);
827
+ -webkit-backdrop-filter: blur(12px);
828
+ }
829
+
830
+ .rd-card-gradient-content-glass-icon {
831
+ inline-size: 4.5rem;
832
+ block-size: 4.5rem;
833
+ min-width: 0;
834
+ padding: 0;
835
+ }
836
+
837
+ .rd-card-gradient-content-glass::before,
838
+ .rd-card-gradient-content-glass::after {
839
+ content: "";
840
+ position: absolute;
841
+ inset: 0;
842
+ z-index: -1;
843
+ pointer-events: none;
844
+ border-radius: inherit;
845
+ }
846
+
847
+ .rd-card-gradient-content-glass::before {
848
+ background: linear-gradient(
849
+ 180deg,
850
+ rgb(255 255 255 / 22%),
851
+ rgb(255 255 255 / 5%) 44%,
852
+ transparent
853
+ );
854
+ opacity: 0.7;
855
+ }
856
+
857
+ .rd-card-gradient-content-glass::after {
858
+ inset: -40% -55%;
859
+ background: linear-gradient(
860
+ 115deg,
861
+ transparent 37%,
862
+ rgb(255 255 255 / 28%) 48%,
863
+ transparent 59%
864
+ );
865
+ opacity: 0;
866
+ transform: translateX(-34%) rotate(7deg);
867
+ }
868
+
869
+ :global(
870
+ [data-rd-card-root][data-rd-card-link="true"]
871
+ .rd-card-gradient-content-glass::after
872
+ ) {
873
+ transition:
874
+ transform 850ms cubic-bezier(0.16, 1, 0.3, 1),
875
+ opacity 850ms cubic-bezier(0.16, 1, 0.3, 1);
876
+ }
877
+
878
+ :global(
879
+ [data-rd-card-root][data-rd-card-link="true"]:hover
880
+ .rd-card-gradient-content-glass::after
881
+ ),
882
+ :global(
883
+ [data-rd-card-root][data-rd-card-link="true"]:focus-within
884
+ .rd-card-gradient-content-glass::after
885
+ ) {
886
+ opacity: 0.68;
887
+ transform: translateX(30%) rotate(7deg);
888
+ }
889
+
890
+ .rd-card-gradient-fallback {
891
+ background:
892
+ radial-gradient(
893
+ 140% 112% at var(--rd-card-gradient-x) var(--rd-card-gradient-y),
894
+ color-mix(
895
+ in oklab,
896
+ var(--rd-card-gradient-color-2) 36%,
897
+ var(--rd-card-gradient-color-1) 64%
898
+ ),
899
+ transparent 62%
900
+ ),
901
+ radial-gradient(
902
+ 128% 108% at var(--rd-card-gradient-x-2) var(--rd-card-gradient-y-2),
903
+ color-mix(
904
+ in oklab,
905
+ var(--rd-card-gradient-color-3) 30%,
906
+ var(--rd-card-gradient-color-1) 70%
907
+ ),
908
+ transparent 68%
909
+ ),
910
+ radial-gradient(
911
+ 110% 92% at 6% 94%,
912
+ color-mix(in oklab, var(--rd-card-gradient-color-4) 24%, white 76%),
913
+ transparent 64%
914
+ ),
915
+ linear-gradient(
916
+ var(--rd-card-gradient-angle),
917
+ color-mix(
918
+ in oklab,
919
+ var(--rd-card-gradient-color-1) 76%,
920
+ var(--rd-card-gradient-color-2) 24%
921
+ ),
922
+ color-mix(
923
+ in oklab,
924
+ var(--rd-card-gradient-color-1) 78%,
925
+ var(--rd-card-gradient-color-4) 22%
926
+ )
927
+ 54%,
928
+ color-mix(
929
+ in oklab,
930
+ var(--rd-card-gradient-color-1) 72%,
931
+ var(--rd-card-gradient-color-3) 28%
932
+ )
933
+ );
934
+ filter: saturate(1.08) blur(0.2px);
935
+ }
936
+
937
+ .rd-card-gradient-scale {
938
+ transform-origin: center;
939
+ }
940
+
941
+ :global(
942
+ [data-rd-card-root][data-rd-card-link="true"] .rd-card-gradient-scale
943
+ ) {
944
+ transition: transform 1000ms cubic-bezier(0.16, 1, 0.3, 1);
945
+ }
946
+
947
+ :global(
948
+ [data-rd-card-root][data-rd-card-link="true"]:hover .rd-card-gradient-scale
949
+ ),
950
+ :global(
951
+ [data-rd-card-root][data-rd-card-link="true"]:focus-within
952
+ .rd-card-gradient-scale
953
+ ) {
954
+ transform: scale(1.03);
955
+ }
956
+
957
+ @media (prefers-reduced-motion: reduce) {
958
+ .rd-card-gradient :global(canvas) {
959
+ transition: none;
960
+ filter: none;
961
+ transform: none;
962
+ }
963
+ }
964
+ </style>