radiant-docs 0.1.24 → 0.1.26
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
|
@@ -9,6 +9,7 @@ import path from "path";
|
|
|
9
9
|
import { getConfig, validateMdxContent } from "./src/lib/validation";
|
|
10
10
|
import remarkCodeBlockComponent from "./src/lib/mdx/remark-code-block-component";
|
|
11
11
|
import remarkDemoteH1 from "./src/lib/mdx/remark-demote-h1";
|
|
12
|
+
import remarkStandaloneImageComponent from "./src/lib/mdx/remark-standalone-image-component";
|
|
12
13
|
import rehypeExternalLinks from "./src/lib/mdx/rehype-external-links";
|
|
13
14
|
import remarkGfm from "remark-gfm";
|
|
14
15
|
import rehypeSlug from "rehype-slug";
|
|
@@ -271,7 +272,12 @@ export default defineConfig({
|
|
|
271
272
|
},
|
|
272
273
|
integrations: [
|
|
273
274
|
mdx({
|
|
274
|
-
remarkPlugins: [
|
|
275
|
+
remarkPlugins: [
|
|
276
|
+
remarkGfm,
|
|
277
|
+
remarkDemoteH1,
|
|
278
|
+
remarkStandaloneImageComponent,
|
|
279
|
+
remarkCodeBlockComponent,
|
|
280
|
+
],
|
|
275
281
|
rehypePlugins: [
|
|
276
282
|
rehypeSlug,
|
|
277
283
|
rehypeExternalLinks,
|
|
@@ -5,9 +5,77 @@ import { renderMarkdown } from "../../lib/utils";
|
|
|
5
5
|
|
|
6
6
|
interface Props extends HTMLAttributes<"img"> {
|
|
7
7
|
src: string;
|
|
8
|
+
zoom?: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
const { title, ...attrs } = Astro.props;
|
|
11
|
+
const { title, zoom = true, ...attrs } = Astro.props as Props;
|
|
12
|
+
const zoomEnabled = zoom !== false;
|
|
13
|
+
const attrsRecord = attrs as Record<string, unknown>;
|
|
14
|
+
const rawStyle = attrsRecord.style;
|
|
15
|
+
const rawWidth = attrsRecord.width;
|
|
16
|
+
const zoomAttrs: Record<string, unknown> = { ...attrsRecord };
|
|
17
|
+
delete zoomAttrs.style;
|
|
18
|
+
delete zoomAttrs.width;
|
|
19
|
+
delete zoomAttrs.height;
|
|
20
|
+
delete zoomAttrs.class;
|
|
21
|
+
delete zoomAttrs.className;
|
|
22
|
+
|
|
23
|
+
function isConstrainedWidthValue(value: unknown): boolean {
|
|
24
|
+
if (typeof value === "number") {
|
|
25
|
+
return Number.isFinite(value) && value > 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof value !== "string") return false;
|
|
29
|
+
const normalized = value.trim().toLowerCase();
|
|
30
|
+
if (normalized.length === 0) return false;
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
normalized === "auto" ||
|
|
34
|
+
normalized === "none" ||
|
|
35
|
+
normalized === "inherit" ||
|
|
36
|
+
normalized === "initial" ||
|
|
37
|
+
normalized === "unset" ||
|
|
38
|
+
normalized === "revert" ||
|
|
39
|
+
normalized === "revert-layer"
|
|
40
|
+
) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (normalized.endsWith("%")) {
|
|
45
|
+
const percent = Number.parseFloat(normalized);
|
|
46
|
+
if (Number.isFinite(percent) && percent >= 100) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasStyleWidthConstraint(styleValue: unknown): boolean {
|
|
55
|
+
if (typeof styleValue === "string") {
|
|
56
|
+
const widthMatch = styleValue.match(/(?:^|;)\s*width\s*:\s*([^;]+)/i);
|
|
57
|
+
const maxWidthMatch = styleValue.match(
|
|
58
|
+
/(?:^|;)\s*max-width\s*:\s*([^;]+)/i,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
isConstrainedWidthValue(widthMatch?.[1]) ||
|
|
63
|
+
isConstrainedWidthValue(maxWidthMatch?.[1])
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!styleValue || typeof styleValue !== "object") return false;
|
|
68
|
+
const styleObject = styleValue as Record<string, unknown>;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
isConstrainedWidthValue(styleObject.width) ||
|
|
72
|
+
isConstrainedWidthValue(styleObject.maxWidth) ||
|
|
73
|
+
isConstrainedWidthValue(styleObject["max-width"])
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const hasCustomImageWidth =
|
|
78
|
+
isConstrainedWidthValue(rawWidth) || hasStyleWidthConstraint(rawStyle);
|
|
11
79
|
|
|
12
80
|
const captionHtml =
|
|
13
81
|
typeof title === "string" && title.trim().length > 0
|
|
@@ -21,19 +89,60 @@ validateProps(
|
|
|
21
89
|
src: { required: true, type: "string" },
|
|
22
90
|
alt: { type: "string" },
|
|
23
91
|
title: { type: "string" },
|
|
92
|
+
zoom: { type: "boolean" },
|
|
24
93
|
},
|
|
25
94
|
Astro.url.pathname,
|
|
26
95
|
);
|
|
27
96
|
---
|
|
28
97
|
|
|
29
98
|
<figure
|
|
30
|
-
class=
|
|
99
|
+
class:list={[
|
|
100
|
+
"p-1.5 pb-1 xs:pb-1.5 group border border-neutral-200/80 dark:border-neutral-800 shadow-xs bg-neutral-50 dark:bg-neutral-900 rounded-2xl",
|
|
101
|
+
hasCustomImageWidth ? "w-fit max-w-full" : "w-full",
|
|
102
|
+
]}
|
|
31
103
|
x-data="{
|
|
32
104
|
open: false,
|
|
33
105
|
showZoomed: false,
|
|
106
|
+
zoomMaxWidth: 1400,
|
|
107
|
+
zoomSize: '',
|
|
34
108
|
style: 'visibility: hidden;',
|
|
35
109
|
fullShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
|
36
110
|
noShadow: '0 0 0 rgba(0, 0, 0, 0)',
|
|
111
|
+
computeZoomSize() {
|
|
112
|
+
const img = this.$refs.img;
|
|
113
|
+
const zoomContainer = this.$refs.zoomViewport;
|
|
114
|
+
if (!img || !zoomContainer) {
|
|
115
|
+
this.zoomSize = `width: min(92vw, ${this.zoomMaxWidth}px);`;
|
|
116
|
+
return this.zoomSize;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const renderedRect = img.getBoundingClientRect();
|
|
120
|
+
const naturalWidth =
|
|
121
|
+
Number(img.naturalWidth) || Number(renderedRect.width) || 1;
|
|
122
|
+
const naturalHeight =
|
|
123
|
+
Number(img.naturalHeight) || Number(renderedRect.height) || 1;
|
|
124
|
+
const containerRect = zoomContainer.getBoundingClientRect();
|
|
125
|
+
const containerStyles = window.getComputedStyle(zoomContainer);
|
|
126
|
+
const horizontalPadding =
|
|
127
|
+
Number.parseFloat(containerStyles.paddingLeft || '0') +
|
|
128
|
+
Number.parseFloat(containerStyles.paddingRight || '0');
|
|
129
|
+
const verticalPadding =
|
|
130
|
+
Number.parseFloat(containerStyles.paddingTop || '0') +
|
|
131
|
+
Number.parseFloat(containerStyles.paddingBottom || '0');
|
|
132
|
+
|
|
133
|
+
const availableWidth = Math.max(1, containerRect.width - horizontalPadding);
|
|
134
|
+
const availableHeight = Math.max(1, containerRect.height - verticalPadding);
|
|
135
|
+
const imageAspectRatio = naturalWidth / naturalHeight;
|
|
136
|
+
const maxWidthByHeight = availableHeight * imageAspectRatio;
|
|
137
|
+
|
|
138
|
+
const targetWidth = Math.max(
|
|
139
|
+
1,
|
|
140
|
+
Math.min(availableWidth, this.zoomMaxWidth, maxWidthByHeight),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
this.zoomSize = `width: ${targetWidth}px;`;
|
|
144
|
+
return this.zoomSize;
|
|
145
|
+
},
|
|
37
146
|
async zoom() {
|
|
38
147
|
// 1. Lock scroll and measure
|
|
39
148
|
document.body.style.overflow = 'hidden';
|
|
@@ -44,6 +153,9 @@ validateProps(
|
|
|
44
153
|
this.open = true;
|
|
45
154
|
this.showZoomed = false;
|
|
46
155
|
|
|
156
|
+
await this.$nextTick();
|
|
157
|
+
this.computeZoomSize();
|
|
158
|
+
this.style = `${this.zoomSize} opacity: 0; transition: none;`;
|
|
47
159
|
await this.$nextTick();
|
|
48
160
|
const zoomed = this.$refs.zoomedImg;
|
|
49
161
|
const zRect = zoomed.getBoundingClientRect();
|
|
@@ -54,17 +166,17 @@ validateProps(
|
|
|
54
166
|
const ty = (rect.top + rect.height/2) - (zRect.top + zRect.height/2);
|
|
55
167
|
|
|
56
168
|
// 4. Snap to initial position (still invisible)
|
|
57
|
-
this.style =
|
|
169
|
+
this.style = `${this.zoomSize} transform: translate(${tx}px, ${ty}px) scale(${scale}); box-shadow: ${this.noShadow}; opacity: 0; transition: none;`;
|
|
58
170
|
|
|
59
171
|
// 5. Triple-frame buffer to ensure paint completion
|
|
60
172
|
requestAnimationFrame(() => {
|
|
61
173
|
// Reveal zoomed image exactly over the thumbnail
|
|
62
|
-
this.style =
|
|
174
|
+
this.style = `${this.zoomSize} transform: translate(${tx}px, ${ty}px) scale(${scale}); box-shadow: ${this.noShadow}; opacity: 1; transition: none;`;
|
|
63
175
|
|
|
64
176
|
requestAnimationFrame(() => {
|
|
65
177
|
// Now start the animation and hide the thumbnail simultaneously
|
|
66
178
|
this.showZoomed = true;
|
|
67
|
-
this.style =
|
|
179
|
+
this.style = `${this.zoomSize} transform: translate(0,0) scale(1); box-shadow: ${this.fullShadow}; opacity: 1; transition: transform 450ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 450ms cubic-bezier(0.4, 0, 0.2, 1);`;
|
|
68
180
|
});
|
|
69
181
|
});
|
|
70
182
|
},
|
|
@@ -77,7 +189,7 @@ validateProps(
|
|
|
77
189
|
const tx = (rect.left + rect.width/2) - (zRect.left + zRect.width/2);
|
|
78
190
|
const ty = (rect.top + rect.height/2) - (zRect.top + zRect.height/2);
|
|
79
191
|
|
|
80
|
-
this.style =
|
|
192
|
+
this.style = `${this.zoomSize} transform: translate(${tx}px, ${ty}px) scale(${scale}); box-shadow: ${this.noShadow}; opacity: 1; transition: transform 400ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 400ms cubic-bezier(0.4, 0, 0.2, 1);`;
|
|
81
193
|
this.showZoomed = false;
|
|
82
194
|
|
|
83
195
|
setTimeout(() => {
|
|
@@ -94,45 +206,54 @@ validateProps(
|
|
|
94
206
|
{...attrs}
|
|
95
207
|
x-ref="img"
|
|
96
208
|
title={title}
|
|
97
|
-
class=
|
|
209
|
+
class:list={[
|
|
210
|
+
"h-auto my-0! block transition-opacity",
|
|
211
|
+
zoomEnabled && "cursor-zoom-in",
|
|
212
|
+
hasCustomImageWidth ? "max-w-full" : "w-full",
|
|
213
|
+
]}
|
|
98
214
|
:class="showZoomed ? 'opacity-0 duration-0' : 'opacity-100 duration-300'"
|
|
99
|
-
@click="zoom()"
|
|
215
|
+
@click={zoomEnabled ? "zoom()" : undefined}
|
|
100
216
|
/>
|
|
101
217
|
</div>
|
|
102
218
|
{
|
|
103
219
|
title && (
|
|
104
|
-
<figcaption class="mt-
|
|
220
|
+
<figcaption class="mt-1! xs:mt-1.5! text-center text-xs! xs:text-sm! text-neutral-500 dark:text-neutral-400 font-medium whitespace-pre-line leading-relaxed px-2">
|
|
105
221
|
<span set:html={captionHtml} />
|
|
106
222
|
</figcaption>
|
|
107
223
|
)
|
|
108
224
|
}
|
|
109
225
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
x-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
226
|
+
{
|
|
227
|
+
zoomEnabled && (
|
|
228
|
+
<template x-teleport="body">
|
|
229
|
+
<div
|
|
230
|
+
x-show="open"
|
|
231
|
+
x-ref="zoomViewport"
|
|
232
|
+
class="fixed bottom-0 top-1 inset-x-1 rounded-t-2xl z-20 flex items-center justify-center pt-20 pb-4 px-4 md:pt-28 md:pb-12 md:px-12 overflow-hidden"
|
|
233
|
+
@keydown.escape.window="close()"
|
|
234
|
+
style="display: none;"
|
|
235
|
+
>
|
|
236
|
+
<div
|
|
237
|
+
x-show="showZoomed"
|
|
238
|
+
x-transition:enter="transition ease-out duration-300"
|
|
239
|
+
x-transition:enter-start="opacity-0"
|
|
240
|
+
x-transition:enter-end="opacity-100"
|
|
241
|
+
x-transition:leave="transition ease-in duration-400"
|
|
242
|
+
x-transition:leave-start="opacity-100"
|
|
243
|
+
x-transition:leave-end="opacity-0"
|
|
244
|
+
class="absolute inset-0 bg-white/90 dark:bg-black/90 backdrop-blur-xl cursor-zoom-out"
|
|
245
|
+
@click="close()"
|
|
246
|
+
>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<img
|
|
250
|
+
{...zoomAttrs}
|
|
251
|
+
x-ref="zoomedImg"
|
|
252
|
+
class="relative z-10 max-w-full max-h-full object-contain rounded-2xl shadow-none will-change-transform pointer-events-none"
|
|
253
|
+
:style="style"
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
</template>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
138
259
|
</figure>
|
|
@@ -6,6 +6,7 @@ import { gfm } from "micromark-extension-gfm";
|
|
|
6
6
|
import { mdxjs } from "micromark-extension-mdxjs";
|
|
7
7
|
import type { Plugin } from "unified";
|
|
8
8
|
import { visitParents } from "unist-util-visit-parents";
|
|
9
|
+
import { transformStandaloneImageParagraphs } from "./remark-standalone-image-component";
|
|
9
10
|
|
|
10
11
|
type CodeNode = {
|
|
11
12
|
type: "code";
|
|
@@ -275,6 +276,7 @@ function parseComponentPreviewChildren(rawCode: string): unknown[] {
|
|
|
275
276
|
mdastExtensions: [gfmFromMarkdown(), mdxFromMarkdown()],
|
|
276
277
|
}) as Root;
|
|
277
278
|
|
|
279
|
+
transformStandaloneImageParagraphs(parsedTree);
|
|
278
280
|
transformCodeBlockNodes(parsedTree);
|
|
279
281
|
|
|
280
282
|
return transformPreviewChildren(
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Root } from "mdast";
|
|
2
|
+
import type { Plugin } from "unified";
|
|
3
|
+
import { visitParents } from "unist-util-visit-parents";
|
|
4
|
+
|
|
5
|
+
type ParagraphNode = {
|
|
6
|
+
type: "paragraph";
|
|
7
|
+
children?: unknown[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ImageNode = {
|
|
11
|
+
type: "image";
|
|
12
|
+
url?: string | null;
|
|
13
|
+
alt?: string | null;
|
|
14
|
+
title?: string | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ParentNode = {
|
|
18
|
+
children?: unknown[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type MdxJsxAttributeNode = {
|
|
22
|
+
type: "mdxJsxAttribute";
|
|
23
|
+
name: string;
|
|
24
|
+
value: string | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type MdxJsxFlowElementNode = {
|
|
28
|
+
type: "mdxJsxFlowElement";
|
|
29
|
+
name: string;
|
|
30
|
+
attributes: MdxJsxAttributeNode[];
|
|
31
|
+
children: unknown[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function createAttribute(name: string, value: string): MdxJsxAttributeNode {
|
|
35
|
+
return {
|
|
36
|
+
type: "mdxJsxAttribute",
|
|
37
|
+
name,
|
|
38
|
+
value,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function transformStandaloneImageParagraphs(tree: Root): void {
|
|
43
|
+
visitParents(tree, "paragraph", (node, ancestors) => {
|
|
44
|
+
const paragraph = node as ParagraphNode;
|
|
45
|
+
if (!Array.isArray(paragraph.children) || paragraph.children.length !== 1) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onlyChild = paragraph.children[0] as ImageNode;
|
|
50
|
+
if (onlyChild.type !== "image") return;
|
|
51
|
+
if (typeof onlyChild.url !== "string" || onlyChild.url.trim().length === 0) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const parent = ancestors[ancestors.length - 1] as ParentNode | undefined;
|
|
56
|
+
if (!Array.isArray(parent?.children)) return;
|
|
57
|
+
|
|
58
|
+
const currentIndex = parent.children.indexOf(node);
|
|
59
|
+
if (currentIndex < 0) return;
|
|
60
|
+
|
|
61
|
+
const attributes: MdxJsxAttributeNode[] = [
|
|
62
|
+
createAttribute("src", onlyChild.url),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
if (typeof onlyChild.alt === "string") {
|
|
66
|
+
attributes.push(createAttribute("alt", onlyChild.alt));
|
|
67
|
+
}
|
|
68
|
+
if (typeof onlyChild.title === "string") {
|
|
69
|
+
attributes.push(createAttribute("title", onlyChild.title));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const replacementNode: MdxJsxFlowElementNode = {
|
|
73
|
+
type: "mdxJsxFlowElement",
|
|
74
|
+
name: "Image",
|
|
75
|
+
attributes,
|
|
76
|
+
children: [],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
parent.children[currentIndex] = replacementNode;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const remarkStandaloneImageComponent: Plugin<[], Root> = () => {
|
|
84
|
+
return (tree) => {
|
|
85
|
+
transformStandaloneImageParagraphs(tree);
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export default remarkStandaloneImageComponent;
|