radiant-docs 0.1.25 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "radiant-docs",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "CLI tool for previewing Radiant documentation locally",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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="p-1.5 pb-2 group border border-neutral-200/80 dark:border-neutral-800 shadow-xs bg-neutral-50 dark:bg-neutral-900 rounded-2xl"
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 = `transform: translate(${tx}px, ${ty}px) scale(${scale}); box-shadow: ${this.noShadow}; opacity: 0; transition: none;`;
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 = `transform: translate(${tx}px, ${ty}px) scale(${scale}); box-shadow: ${this.noShadow}; opacity: 1; transition: none;`;
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 = `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);`;
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 = `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);`;
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="w-full h-auto my-0! block cursor-zoom-in transition-opacity"
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-2 text-center text-sm text-neutral-500 dark:text-neutral-400 font-medium whitespace-pre-line leading-relaxed px-2">
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
- <template x-teleport="body">
111
- <div
112
- x-show="open"
113
- 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"
114
- @keydown.escape.window="close()"
115
- style="display: none;"
116
- >
117
- <div
118
- x-show="showZoomed"
119
- x-transition:enter="transition ease-out duration-300"
120
- x-transition:enter-start="opacity-0"
121
- x-transition:enter-end="opacity-100"
122
- x-transition:leave="transition ease-in duration-400"
123
- x-transition:leave-start="opacity-100"
124
- x-transition:leave-end="opacity-0"
125
- class="absolute inset-0 bg-white/90 dark:bg-black/90 backdrop-blur-xl cursor-zoom-out"
126
- @click="close()"
127
- >
128
- </div>
129
-
130
- <img
131
- {...attrs}
132
- x-ref="zoomedImg"
133
- class="relative z-10 max-w-full max-h-full object-contain rounded-2xl shadow-none will-change-transform pointer-events-none"
134
- :style="style"
135
- />
136
- </div>
137
- </template>
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>