From c6dae2cd0f58ab7e8ccc9869ef9bc15c1fb398df Mon Sep 17 00:00:00 2001 From: Abreham Gezahegn Date: Tue, 10 Jun 2025 23:47:48 +0300 Subject: [PATCH 1/4] add image resolver prop call resolver when image is in view --- typescript/package-lock.json | 15 +++ typescript/package.json | 1 + typescript/src/renderer/JsonDocRenderer.tsx | 3 + .../src/renderer/components/BlockRenderer.tsx | 6 +- .../components/blocks/ImageBlockRenderer.tsx | 105 +++++++++++++++--- typescript/src/renderer/styles/media.css | 89 ++++++++++++++- typescript/src/renderer/styles/variables.css | 2 +- 7 files changed, 205 insertions(+), 16 deletions(-) diff --git a/typescript/package-lock.json b/typescript/package-lock.json index 799c8f0..4c3313d 100644 --- a/typescript/package-lock.json +++ b/typescript/package-lock.json @@ -14,6 +14,7 @@ "json5": "^2.2.3", "katex": "^0.16.22", "puppeteer": "^24.9.0", + "react-intersection-observer": "^9.16.0", "strip-json-comments": "^5.0.2" }, "devDependencies": { @@ -9318,6 +9319,20 @@ "react": "^18.3.1" } }, + "node_modules/react-intersection-observer": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", + "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", diff --git a/typescript/package.json b/typescript/package.json index d2415a5..9ee2d24 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -68,6 +68,7 @@ "json5": "^2.2.3", "katex": "^0.16.22", "puppeteer": "^24.9.0", + "react-intersection-observer": "^9.16.0", "strip-json-comments": "^5.0.2" }, "peerDependencies": { diff --git a/typescript/src/renderer/JsonDocRenderer.tsx b/typescript/src/renderer/JsonDocRenderer.tsx index 1cc86c1..5a4d3cb 100644 --- a/typescript/src/renderer/JsonDocRenderer.tsx +++ b/typescript/src/renderer/JsonDocRenderer.tsx @@ -8,6 +8,7 @@ interface JsonDocRendererProps { className?: string; components?: React.ComponentProps["components"]; theme?: "light" | "dark"; + resolveImageUrl?: (url: string) => Promise; } export const JsonDocRenderer = ({ @@ -15,6 +16,7 @@ export const JsonDocRenderer = ({ className = "", components, theme = "light", + resolveImageUrl, }: JsonDocRendererProps) => { return (
@@ -42,6 +44,7 @@ export const JsonDocRenderer = ({ block={block} depth={0} components={components} + resolveImageUrl={resolveImageUrl} /> ))}
diff --git a/typescript/src/renderer/components/BlockRenderer.tsx b/typescript/src/renderer/components/BlockRenderer.tsx index f426ba5..f34e140 100644 --- a/typescript/src/renderer/components/BlockRenderer.tsx +++ b/typescript/src/renderer/components/BlockRenderer.tsx @@ -56,12 +56,14 @@ interface BlockRendererProps { block: any; depth?: number; components?: BlockComponents; + resolveImageUrl?: (url: string) => Promise; } export const BlockRenderer: React.FC = ({ block, depth = 0, components, + resolveImageUrl, }) => { const commonProps = { block, depth, components }; @@ -106,7 +108,9 @@ export const BlockRenderer: React.FC = ({ // Image block if (block?.type === "image") { const ImageComponent = components?.image || ImageBlockRenderer; - return ; + return ( + + ); } // Table blocks diff --git a/typescript/src/renderer/components/blocks/ImageBlockRenderer.tsx b/typescript/src/renderer/components/blocks/ImageBlockRenderer.tsx index b2f77be..73dc0cb 100644 --- a/typescript/src/renderer/components/blocks/ImageBlockRenderer.tsx +++ b/typescript/src/renderer/components/blocks/ImageBlockRenderer.tsx @@ -1,12 +1,31 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { useInView } from "react-intersection-observer"; import { RichTextRenderer } from "../RichTextRenderer"; import { BlockRenderer } from "../BlockRenderer"; +const ImagePlaceholderIcon: React.FC = () => ( + + + + + +); + interface ImageBlockRendererProps extends React.HTMLAttributes { block: any; depth?: number; components?: React.ComponentProps["components"]; + resolveImageUrl?: (url: string) => Promise; } export const ImageBlockRenderer: React.FC = ({ @@ -14,9 +33,14 @@ export const ImageBlockRenderer: React.FC = ({ depth = 0, className, components, + resolveImageUrl, ...props }) => { const imageData = block.image; + const [url, setUrl] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [hasError, setHasError] = useState(false); + const { ref, inView } = useInView({ threshold: 0.1, triggerOnce: true }); const getImageUrl = () => { if (imageData?.type === "external") { @@ -29,6 +53,38 @@ export const ImageBlockRenderer: React.FC = ({ const imageUrl = getImageUrl(); + useEffect(() => { + let cancelled = false; + + const imageUrlEffect = async () => { + if (resolveImageUrl && imageUrl) { + setIsLoading(true); + setHasError(false); + try { + const url_ = await resolveImageUrl(imageUrl); + if (!cancelled) { + setUrl(url_); + setIsLoading(false); + } + } catch (error) { + if (!cancelled) { + setHasError(true); + setIsLoading(false); + console.error("Failed to resolve image URL:", error); + } + } + } + }; + + if (inView && imageUrl) { + imageUrlEffect(); + } + + return () => { + cancelled = true; + }; + }, [inView, imageUrl, resolveImageUrl]); + return (
= ({ >
-
-
- {imageUrl && ( - - )} -
+
+ {imageUrl && ( +
+ {(isLoading || (!url && resolveImageUrl)) && !hasError && ( +
+
+
+ +
+
Loading image...
+
+
+ )} + {hasError && ( +
+
Failed to load image
+
+ )} + {!isLoading && !hasError && (url || !resolveImageUrl) && ( + {imageData?.caption setHasError(true)} + /> + )} +
+ )}
{/* Caption */} {imageData?.caption && imageData.caption.length > 0 && ( -
+
-
+ )}
diff --git a/typescript/src/renderer/styles/media.css b/typescript/src/renderer/styles/media.css index ba17b10..e5770fd 100644 --- a/typescript/src/renderer/styles/media.css +++ b/typescript/src/renderer/styles/media.css @@ -2,6 +2,88 @@ .notion-image-block { padding: var(--jsondoc-spacing-sm) var(--jsondoc-spacing-xs); + margin: var(--jsondoc-spacing-md) 0; +} + +.notion-image-block figure, +.notion-image-block [role="figure"] { + margin: 0; + padding: 0; +} + +.notion-image-block img { + max-width: 100%; + height: auto; + display: block; + border-radius: var(--jsondoc-radius-sm); + box-shadow: + rgba(15, 15, 15, 0.1) 0px 0px 0px 1px, + rgba(15, 15, 15, 0.1) 0px 2px 4px; +} + +/* Image Block Loading States */ +.image-loading-placeholder { + width: 100%; + height: 300px; + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: 12px; + position: relative; + overflow: hidden; +} + +.image-loading-content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.image-loading-icon { + width: 48px; + height: 48px; + border-radius: 50%; + background: rgba(107, 114, 128, 0.1); + display: flex; + align-items: center; + justify-content: center; +} + +.image-loading-text { + color: #9ca3af; + font-size: 14px; + font-weight: 500; +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +/* Image Block Error State */ +.image-error-placeholder { + width: 100%; + height: 300px; + background-color: #fef2f2; + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + border: 1px solid #fecaca; +} + +.image-error-text { + color: #dc2626; + font-size: 14px; } .notion-image-placeholder { @@ -38,7 +120,12 @@ } .notion-image-caption { - color: var(--jsondoc-text-primary); + color: var(--jsondoc-text-muted); font-size: var(--jsondoc-font-size-caption); + line-height: var(--jsondoc-line-height-relaxed); + text-align: center; margin-top: var(--jsondoc-spacing-md); + margin-bottom: var(--jsondoc-spacing-sm); + font-style: italic; + max-width: 100%; } diff --git a/typescript/src/renderer/styles/variables.css b/typescript/src/renderer/styles/variables.css index 7e7319d..ffbb218 100644 --- a/typescript/src/renderer/styles/variables.css +++ b/typescript/src/renderer/styles/variables.css @@ -43,7 +43,7 @@ --jsondoc-font-size-h2: clamp(1.25rem, 3vw, 1.5rem); --jsondoc-font-size-h3: clamp(1.125rem, 2.5vw, 1.25rem); --jsondoc-font-size-body: 1rem; - --jsondoc-font-size-caption: 14px; + --jsondoc-font-size-caption: 12px; --jsondoc-font-size-code: 14px; --jsondoc-font-size-code-language: 12px; --jsondoc-font-size-inline-code: 85%; From d9c12898008af3b1425cab48ba226c397210e2d1 Mon Sep 17 00:00:00 2001 From: Abreham Gezahegn Date: Tue, 10 Jun 2025 23:48:32 +0300 Subject: [PATCH 2/4] bump config --- typescript/bump.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/typescript/bump.config.ts b/typescript/bump.config.ts index 23c6ab2..49f5f79 100644 --- a/typescript/bump.config.ts +++ b/typescript/bump.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from "bumpp"; export default defineConfig({ - // ...options + commit: false, + tag: "typescript-v%s", }); From 08011265ee66b308bd979e4426fe457abbabe104 Mon Sep 17 00:00:00 2001 From: Abreham Gezahegn Date: Tue, 10 Jun 2025 23:52:50 +0300 Subject: [PATCH 3/4] checkpoint --- typescript/bump.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typescript/bump.config.ts b/typescript/bump.config.ts index 49f5f79..a590db4 100644 --- a/typescript/bump.config.ts +++ b/typescript/bump.config.ts @@ -3,4 +3,5 @@ import { defineConfig } from "bumpp"; export default defineConfig({ commit: false, tag: "typescript-v%s", + push: false, }); From 64e7c9aa65f98c92bc8661ad04f22d9a75503fe0 Mon Sep 17 00:00:00 2001 From: Abreham Gezahegn Date: Tue, 10 Jun 2025 23:53:12 +0300 Subject: [PATCH 4/4] checkpoint --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index c49580b..bf62725 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -98,7 +98,7 @@ jobs: working-directory: ./typescript run: npm ci - - name: Extract version from tag and update package.json + - name: Extract version from tag and update npm version working-directory: ./typescript run: | # Get the version from the tag (remove 'typescript-v' prefix)