Skip to content

Commit ee69234

Browse files
jelllidimaMachina
andauthoredDec 20, 2024··
add image zoom component (#3829)
* add image zoom component * tweaks * lint * style tweaks * fix ssr error * do not add zoom for images inside anchor * polish * polish * polish * final polish * add doc * Create sweet-ears-pump.md * lint * polish * polish * lgtm * Update docs/pages/docs/guide/image.mdx --------- Co-authored-by: Dimitri POSTOLOV <dmytropostolov@gmail.com>
1 parent ac70dfe commit ee69234

File tree

14 files changed

+436
-706
lines changed

14 files changed

+436
-706
lines changed
 

‎.changeset/sweet-ears-pump.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"nextra-theme-blog": minor
3+
"nextra-theme-docs": minor
4+
"nextra": minor
5+
---
6+
7+
add [image zoom feature](http://nextra.site/docs/guide/image#image-zoom) for all images written via [GFM syntax](https://github.github.com/gfm/#images)
8+
in md/mdx files (except images inside links)

‎docs/next-env.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
/// <reference types="next/navigation-types/compat/navigation" />
44

55
// NOTE: This file should not be edited
6-
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
6+
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

‎docs/pages/docs/guide/image.mdx

+43
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,46 @@ With Next.js Image, there will be no layout shift, and a beautiful blurry
3939
placeholder will be shown by default when loading the images:
4040

4141
![Nextra](/og.jpeg)
42+
43+
## Image Zoom
44+
45+
<Callout>The image zoom feature is enabled globally by default.</Callout>
46+
47+
In the default configuration, if you want to use this feature, simply insert
48+
images using `![]()` Markdown syntax.
49+
50+
### Disable Image Zoom
51+
52+
For `nextra-docs-theme` and `nextra-blog-theme`, you can disable image zoom by
53+
replacing the `img` component used in MDX.
54+
55+
```jsx filename="theme.config.jsx"
56+
import { Image } from 'nextra/components'
57+
58+
export default {
59+
// ... your other configurations
60+
components: {
61+
img: props => <Image {...props} />
62+
}
63+
}
64+
```
65+
66+
### Enable/Disable Image Zoom for Specific Images
67+
68+
When zoom is **disabled globally**, but you want to enable it for specific
69+
images, you can do so by using the `<ImageZoom>` component:
70+
71+
```mdx
72+
import { ImageZoom } from 'nextra/components'
73+
74+
<ImageZoom src="/demo.png" />
75+
```
76+
77+
When zoom is **enabled globally**, and you want to disable zoom for a specific
78+
image, you can simply use the `<Image>` component:
79+
80+
```mdx
81+
import { Image } from 'nextra/components'
82+
83+
<Image src="/demo.png" />
84+
```

‎package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"version": "changeset version"
2323
},
2424
"devDependencies": {
25-
"@changesets/cli": "2.27.9",
25+
"@changesets/cli": "2.27.11",
2626
"@ianvs/prettier-plugin-sort-imports": "4.4.0",
2727
"@next/eslint-plugin-next": "15.1.0",
2828
"@rollup/plugin-alias": "^5.1.1",
@@ -53,7 +53,7 @@
5353
"next": "15.1.0"
5454
},
5555
"patchedDependencies": {
56-
"@changesets/assemble-release-plan@6.0.4": "patches/@changesets__assemble-release-plan.patch",
56+
"@changesets/assemble-release-plan@6.0.5": "patches/@changesets__assemble-release-plan.patch",
5757
"eslint-plugin-deprecation@3.0.0": "patches/eslint-plugin-deprecation.patch",
5858
"tsup@8.3.5": "patches/tsup@8.3.5.patch"
5959
}

‎packages/nextra-theme-blog/src/styles.css

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
@import 'nextra/styles/scrollbar.css';
77
@import 'nextra/styles/steps.css';
88
@import 'nextra/styles/cards.css';
9+
@import 'nextra/styles/react-medium-image-zoom.css';
910

1011
html {
1112
@apply _scroll-pt-5;
@@ -32,6 +33,7 @@ h1 {
3233
&:after {
3334
@apply _hidden;
3435
}
36+
3537
.line {
3638
@apply _font-normal;
3739
}

‎packages/nextra-theme-docs/css/styles.css

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
@import 'nextra/styles/scrollbar.css';
77
@import 'nextra/styles/steps.css';
88
@import 'nextra/styles/cards.css';
9+
@import 'nextra/styles/react-medium-image-zoom.css';
910
@import './hamburger.css';
1011
@import './typesetting-article.css';
1112

‎packages/nextra/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
"mdast-util-to-hast": "^13.2.0",
151151
"negotiator": "^1.0.0",
152152
"p-limit": "^6.0.0",
153+
"react-medium-image-zoom": "^5.2.12",
153154
"rehype-katex": "^7.0.0",
154155
"rehype-pretty-code": "0.14.0",
155156
"rehype-raw": "^7.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use client'
2+
3+
import { type ImageProps } from 'next/image'
4+
import { useEffect, useRef, useState, type FC } from 'react'
5+
import Zoom from 'react-medium-image-zoom'
6+
import { Image } from './image.js'
7+
8+
function getImageSrc(src: ImageProps['src']): string {
9+
if (typeof src === 'string') {
10+
return src
11+
}
12+
if ('default' in src) {
13+
return src.default.src
14+
}
15+
return src.src
16+
}
17+
18+
export const ImageZoom: FC<ImageProps> = props => {
19+
const imgRef = useRef<HTMLImageElement>(null!)
20+
const [isInsideAnchor, setIsInsideAnchor] = useState(false)
21+
22+
useEffect(() => {
23+
setIsInsideAnchor(imgRef.current.closest('a') !== null)
24+
}, [])
25+
26+
const img = <Image {...props} ref={imgRef} />
27+
28+
if (isInsideAnchor) {
29+
// There is no need to add zoom for images inside anchor tags
30+
return img
31+
}
32+
33+
return (
34+
<Zoom
35+
zoomMargin={40}
36+
zoomImg={{
37+
src: getImageSrc(props.src),
38+
alt: props.alt
39+
}}
40+
// fix Expected server HTML to contain a matching <div> in <p>.
41+
wrapElement="span"
42+
>
43+
{img}
44+
</Zoom>
45+
)
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { ImageProps } from 'next/image'
2+
import NextImage from 'next/image'
3+
import { forwardRef } from 'react'
4+
5+
export const Image = forwardRef<HTMLImageElement, ImageProps>((props, ref) => {
6+
const ComponentToUse = typeof props.src === 'object' ? NextImage : 'img'
7+
8+
// @ts-expect-error -- fixme
9+
return <ComponentToUse {...props} ref={ref} />
10+
})
11+
12+
Image.displayName = 'Image'

‎packages/nextra/src/client/components/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export { Cards } from './cards.js'
44
export { Code } from './code.js'
55
export { CopyToClipboard } from './copy-to-clipboard.js'
66
export { FileTree } from './file-tree.js'
7+
export { Image } from './image.js'
8+
export { ImageZoom } from './image-zoom.js'
79
export { Pre } from './pre.js'
810
export { RemoteContent, evaluate } from './remote-content.js'
911
export { Steps } from './steps.js'

‎packages/nextra/src/client/mdx.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
import { useMDXComponents as originalUseMDXComponents } from '@mdx-js/react'
2-
import Image, { type ImageProps } from 'next/image'
3-
import { createElement } from 'react'
2+
import type { ComponentPropsWithoutRef, FC } from 'react'
3+
import { ImageZoom } from './components/image-zoom.js'
44

55
type MDXComponents = ReturnType<typeof originalUseMDXComponents>
66

77
const DEFAULT_COMPONENTS = {
8-
img: props =>
9-
createElement(
10-
typeof props.src === 'object' ? Image : 'img',
11-
props as ImageProps
12-
)
8+
img: ImageZoom as FC<ComponentPropsWithoutRef<'img'>>
139
} satisfies MDXComponents
1410

1511
export const useMDXComponents: typeof originalUseMDXComponents = components => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
[data-rmiz-ghost] {
2+
position: absolute;
3+
pointer-events: none;
4+
}
5+
6+
[data-rmiz-btn-zoom],
7+
[data-rmiz-btn-unzoom] {
8+
@apply _bg-black/70 _text-white dark:_bg-white/70 dark:_text-black _rounded-full _size-10;
9+
outline-offset: 2px;
10+
padding: 9px;
11+
touch-action: manipulation;
12+
appearance: none;
13+
}
14+
15+
[data-rmiz-btn-zoom] {
16+
position: absolute;
17+
inset: 10px 10px auto auto;
18+
cursor: zoom-in;
19+
20+
&:not(:focus):not(:active) {
21+
position: absolute;
22+
clip: rect(0 0 0 0);
23+
clip-path: inset(50%);
24+
height: 1px;
25+
overflow: hidden;
26+
pointer-events: none;
27+
white-space: nowrap;
28+
width: 1px;
29+
}
30+
}
31+
32+
[data-rmiz-btn-unzoom] {
33+
position: absolute;
34+
inset: 20px 20px auto auto;
35+
cursor: zoom-out;
36+
z-index: 1;
37+
}
38+
39+
[data-rmiz-content='found'] {
40+
img,
41+
svg,
42+
[role='img'],
43+
[data-zoom] {
44+
cursor: zoom-in;
45+
}
46+
}
47+
48+
[data-rmiz-modal]::backdrop {
49+
display: none;
50+
}
51+
52+
[data-rmiz-modal][open] {
53+
position: fixed;
54+
width: 100vw;
55+
width: 100dvw;
56+
height: 100vh;
57+
height: 100dvh;
58+
max-width: none;
59+
max-height: none;
60+
margin: 0;
61+
padding: 0;
62+
border: 0;
63+
background: transparent;
64+
overflow: hidden;
65+
}
66+
67+
[data-rmiz-modal-overlay] {
68+
position: absolute;
69+
inset: 0;
70+
transition: opacity 0.3s;
71+
background: rgba(var(--nextra-bg), 0.8);
72+
}
73+
74+
[data-rmiz-modal-overlay='hidden'] {
75+
opacity: 0%;
76+
}
77+
78+
[data-rmiz-modal-overlay='visible'] {
79+
opacity: 100%;
80+
}
81+
82+
[data-rmiz-modal-content] {
83+
position: relative;
84+
width: 100%;
85+
height: 100%;
86+
}
87+
88+
[data-rmiz-modal-img] {
89+
position: absolute;
90+
cursor: zoom-out;
91+
image-rendering: high-quality;
92+
transform-origin: top left;
93+
transition: transform 0.3s;
94+
}
95+
96+
@media (prefers-reduced-motion: reduce) {
97+
[data-rmiz-modal-overlay],
98+
[data-rmiz-modal-img] {
99+
transition-duration: 0.01ms;
100+
}
101+
}

‎patches/@changesets__assemble-release-plan.patch

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
diff --git a/dist/changesets-assemble-release-plan.cjs.js b/dist/changesets-assemble-release-plan.cjs.js
2-
index 4f7b5e5b37bb05874a5c1d8e583e29d4a9593ecf..118d3eaaa2e0615b6813f48fe2618bfebd82a5ca 100644
2+
index e32a5e5d39c3bd920201b5694632d2b44c92d486..08b9a101d13d112cfaa39f91fdb3a5e8c29bbedc 100644
33
--- a/dist/changesets-assemble-release-plan.cjs.js
44
+++ b/dist/changesets-assemble-release-plan.cjs.js
55
@@ -347,7 +347,7 @@ function shouldBumpMajor({

‎pnpm-lock.yaml

+213-695
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.