Skip to content

Commit c2f0972

Browse files
Image url resolve (#20)
* add image resolver prop call resolver when image is in view * bump config * checkpoint * checkpoint
1 parent d16515e commit c2f0972

File tree

9 files changed

+209
-18
lines changed

9 files changed

+209
-18
lines changed

.github/workflows/publish.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ jobs:
9898
working-directory: ./typescript
9999
run: npm ci
100100

101-
- name: Extract version from tag and update package.json
101+
- name: Extract version from tag and update npm version
102102
working-directory: ./typescript
103103
run: |
104104
# Get the version from the tag (remove 'typescript-v' prefix)

typescript/bump.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { defineConfig } from "bumpp";
22

33
export default defineConfig({
4-
// ...options
4+
commit: false,
5+
tag: "typescript-v%s",
6+
push: false,
57
});

typescript/package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

typescript/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"json5": "^2.2.3",
6969
"katex": "^0.16.22",
7070
"puppeteer": "^24.9.0",
71+
"react-intersection-observer": "^9.16.0",
7172
"strip-json-comments": "^5.0.2"
7273
},
7374
"peerDependencies": {

typescript/src/renderer/JsonDocRenderer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ interface JsonDocRendererProps {
88
className?: string;
99
components?: React.ComponentProps<typeof BlockRenderer>["components"];
1010
theme?: "light" | "dark";
11+
resolveImageUrl?: (url: string) => Promise<string>;
1112
}
1213

1314
export const JsonDocRenderer = ({
1415
page,
1516
className = "",
1617
components,
1718
theme = "light",
19+
resolveImageUrl,
1820
}: JsonDocRendererProps) => {
1921
return (
2022
<div className={`json-doc-renderer jsondoc-theme-${theme} ${className}`}>
@@ -42,6 +44,7 @@ export const JsonDocRenderer = ({
4244
block={block}
4345
depth={0}
4446
components={components}
47+
resolveImageUrl={resolveImageUrl}
4548
/>
4649
))}
4750
</div>

typescript/src/renderer/components/BlockRenderer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,14 @@ interface BlockRendererProps {
5656
block: any;
5757
depth?: number;
5858
components?: BlockComponents;
59+
resolveImageUrl?: (url: string) => Promise<string>;
5960
}
6061

6162
export const BlockRenderer: React.FC<BlockRendererProps> = ({
6263
block,
6364
depth = 0,
6465
components,
66+
resolveImageUrl,
6567
}) => {
6668
const commonProps = { block, depth, components };
6769

@@ -106,7 +108,9 @@ export const BlockRenderer: React.FC<BlockRendererProps> = ({
106108
// Image block
107109
if (block?.type === "image") {
108110
const ImageComponent = components?.image || ImageBlockRenderer;
109-
return <ImageComponent {...commonProps} />;
111+
return (
112+
<ImageComponent {...commonProps} resolveImageUrl={resolveImageUrl} />
113+
);
110114
}
111115

112116
// Table blocks

typescript/src/renderer/components/blocks/ImageBlockRenderer.tsx

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,46 @@
1-
import React from "react";
1+
import React, { useEffect, useState } from "react";
2+
import { useInView } from "react-intersection-observer";
23

34
import { RichTextRenderer } from "../RichTextRenderer";
45
import { BlockRenderer } from "../BlockRenderer";
56

7+
const ImagePlaceholderIcon: React.FC = () => (
8+
<svg
9+
width="24"
10+
height="24"
11+
viewBox="0 0 24 24"
12+
fill="none"
13+
stroke="#9ca3af"
14+
strokeWidth="2"
15+
strokeLinecap="round"
16+
strokeLinejoin="round"
17+
>
18+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
19+
<circle cx="9" cy="9" r="2" />
20+
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
21+
</svg>
22+
);
23+
624
interface ImageBlockRendererProps extends React.HTMLAttributes<HTMLDivElement> {
725
block: any;
826
depth?: number;
927
components?: React.ComponentProps<typeof BlockRenderer>["components"];
28+
resolveImageUrl?: (url: string) => Promise<string>;
1029
}
1130

1231
export const ImageBlockRenderer: React.FC<ImageBlockRendererProps> = ({
1332
block,
1433
depth = 0,
1534
className,
1635
components,
36+
resolveImageUrl,
1737
...props
1838
}) => {
1939
const imageData = block.image;
40+
const [url, setUrl] = useState<string>();
41+
const [isLoading, setIsLoading] = useState<boolean>(false);
42+
const [hasError, setHasError] = useState<boolean>(false);
43+
const { ref, inView } = useInView({ threshold: 0.1, triggerOnce: true });
2044

2145
const getImageUrl = () => {
2246
if (imageData?.type === "external") {
@@ -29,6 +53,38 @@ export const ImageBlockRenderer: React.FC<ImageBlockRendererProps> = ({
2953

3054
const imageUrl = getImageUrl();
3155

56+
useEffect(() => {
57+
let cancelled = false;
58+
59+
const imageUrlEffect = async () => {
60+
if (resolveImageUrl && imageUrl) {
61+
setIsLoading(true);
62+
setHasError(false);
63+
try {
64+
const url_ = await resolveImageUrl(imageUrl);
65+
if (!cancelled) {
66+
setUrl(url_);
67+
setIsLoading(false);
68+
}
69+
} catch (error) {
70+
if (!cancelled) {
71+
setHasError(true);
72+
setIsLoading(false);
73+
console.error("Failed to resolve image URL:", error);
74+
}
75+
}
76+
}
77+
};
78+
79+
if (inView && imageUrl) {
80+
imageUrlEffect();
81+
}
82+
83+
return () => {
84+
cancelled = true;
85+
};
86+
}, [inView, imageUrl, resolveImageUrl]);
87+
3288
return (
3389
<div
3490
{...props}
@@ -37,24 +93,47 @@ export const ImageBlockRenderer: React.FC<ImageBlockRendererProps> = ({
3793
>
3894
<div className="notion-selectable-container">
3995
<div role="figure">
40-
<div className="notion-cursor-default">
41-
<div>
42-
{imageUrl && (
43-
<img
44-
alt=""
45-
src={imageUrl}
46-
style={{ maxWidth: "100%", height: "auto" }}
47-
/>
48-
)}
49-
</div>
96+
<div className="notion-cursor-default" ref={ref}>
97+
{imageUrl && (
98+
<div
99+
style={{
100+
position: "relative",
101+
width: "100%",
102+
maxWidth: "600px",
103+
}}
104+
>
105+
{(isLoading || (!url && resolveImageUrl)) && !hasError && (
106+
<div className="image-loading-placeholder">
107+
<div className="image-loading-content">
108+
<div className="image-loading-icon">
109+
<ImagePlaceholderIcon />
110+
</div>
111+
<div className="image-loading-text">Loading image...</div>
112+
</div>
113+
</div>
114+
)}
115+
{hasError && (
116+
<div className="image-error-placeholder">
117+
<div className="image-error-text">Failed to load image</div>
118+
</div>
119+
)}
120+
{!isLoading && !hasError && (url || !resolveImageUrl) && (
121+
<img
122+
alt={imageData?.caption ? "" : "Image"}
123+
src={url || imageUrl}
124+
onError={() => setHasError(true)}
125+
/>
126+
)}
127+
</div>
128+
)}
50129
</div>
51130
{/* Caption */}
52131
{imageData?.caption && imageData.caption.length > 0 && (
53-
<div>
132+
<figcaption className="notion-image-caption">
54133
<div className="notranslate">
55134
<RichTextRenderer richText={imageData.caption} />
56135
</div>
57-
</div>
136+
</figcaption>
58137
)}
59138
</div>
60139
</div>

typescript/src/renderer/styles/media.css

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,88 @@
22

33
.notion-image-block {
44
padding: var(--jsondoc-spacing-sm) var(--jsondoc-spacing-xs);
5+
margin: var(--jsondoc-spacing-md) 0;
6+
}
7+
8+
.notion-image-block figure,
9+
.notion-image-block [role="figure"] {
10+
margin: 0;
11+
padding: 0;
12+
}
13+
14+
.notion-image-block img {
15+
max-width: 100%;
16+
height: auto;
17+
display: block;
18+
border-radius: var(--jsondoc-radius-sm);
19+
box-shadow:
20+
rgba(15, 15, 15, 0.1) 0px 0px 0px 1px,
21+
rgba(15, 15, 15, 0.1) 0px 2px 4px;
22+
}
23+
24+
/* Image Block Loading States */
25+
.image-loading-placeholder {
26+
width: 100%;
27+
height: 300px;
28+
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
29+
background-size: 200% 100%;
30+
animation: shimmer 1.5s infinite;
31+
border-radius: 12px;
32+
position: relative;
33+
overflow: hidden;
34+
}
35+
36+
.image-loading-content {
37+
position: absolute;
38+
top: 50%;
39+
left: 50%;
40+
transform: translate(-50%, -50%);
41+
display: flex;
42+
flex-direction: column;
43+
align-items: center;
44+
gap: 12px;
45+
}
46+
47+
.image-loading-icon {
48+
width: 48px;
49+
height: 48px;
50+
border-radius: 50%;
51+
background: rgba(107, 114, 128, 0.1);
52+
display: flex;
53+
align-items: center;
54+
justify-content: center;
55+
}
56+
57+
.image-loading-text {
58+
color: #9ca3af;
59+
font-size: 14px;
60+
font-weight: 500;
61+
}
62+
63+
@keyframes shimmer {
64+
0% {
65+
background-position: -200% 0;
66+
}
67+
100% {
68+
background-position: 200% 0;
69+
}
70+
}
71+
72+
/* Image Block Error State */
73+
.image-error-placeholder {
74+
width: 100%;
75+
height: 300px;
76+
background-color: #fef2f2;
77+
display: flex;
78+
align-items: center;
79+
justify-content: center;
80+
border-radius: 12px;
81+
border: 1px solid #fecaca;
82+
}
83+
84+
.image-error-text {
85+
color: #dc2626;
86+
font-size: 14px;
587
}
688

789
.notion-image-placeholder {
@@ -38,7 +120,12 @@
38120
}
39121

40122
.notion-image-caption {
41-
color: var(--jsondoc-text-primary);
123+
color: var(--jsondoc-text-muted);
42124
font-size: var(--jsondoc-font-size-caption);
125+
line-height: var(--jsondoc-line-height-relaxed);
126+
text-align: center;
43127
margin-top: var(--jsondoc-spacing-md);
128+
margin-bottom: var(--jsondoc-spacing-sm);
129+
font-style: italic;
130+
max-width: 100%;
44131
}

typescript/src/renderer/styles/variables.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
--jsondoc-font-size-h2: clamp(1.25rem, 3vw, 1.5rem);
4444
--jsondoc-font-size-h3: clamp(1.125rem, 2.5vw, 1.25rem);
4545
--jsondoc-font-size-body: 1rem;
46-
--jsondoc-font-size-caption: 14px;
46+
--jsondoc-font-size-caption: 12px;
4747
--jsondoc-font-size-code: 14px;
4848
--jsondoc-font-size-code-language: 12px;
4949
--jsondoc-font-size-inline-code: 85%;

0 commit comments

Comments
 (0)