Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Single `BaseType` covers all. Grouped here by purpose:
| Preset | `preset: 'desktop' \| 'mobile' \| 'auto'` | defaults to `'auto'` when omitted; drives default `controller` / `hotKey` / `animate` / `gesture` plus preset-aware viewer spacing. `'auto'` resolves via `matchMedia('(pointer: coarse) and (hover: none)')` on the client; SSR falls back to desktop |
| Controlled | `browsing` | omit for self-managed; pair with `onBrowsing` if set. Does not control `<Zmage.Wrapper>` |
| Functional | `controller`, `hotKey`, `animate`, `gesture` | pass `boolean` to disable, or partial object to override. `controller.flip` / `hotKey.flip` and `controller.rotate` / `hotKey.rotate` are umbrellas over their per-side counterparts; enabling the umbrella forces both sides on. `controller.placement` moves only the toolbar capsule (`top-right` default); side flips and pagination keep their existing positions. `controller.layout` adjusts toolbar / flip / pagination / caption overlay safe insets without changing image animation geometry; number = px, string = CSS length, scalar `inset` follows each target's natural edge (toolbar by placement, flip left/right, pagination/caption bottom), and `layout.mobile` overrides base layout for mobile preset. Desktop defaults include `pagination.inset=24` and `caption.inset=60`; mobile leaves `layout` unset unless configured. `controller.render({ state, actions, slots })` replaces the whole controller UI, and `controller=false` disables both built-in slots and render. **`hotKey` entries accept `boolean \| string \| string[]`** — string is an `e.code` descriptor (`'Escape'` / `'BracketLeft'` / `'S'`) with cross-platform `Mod` prefix (= ⌘ on macOS, Ctrl elsewhere; e.g. `'Mod+S'`). New defaults: `[`/`]` rotate, `Mod+S` download (download is opt-in: defaults `false` because it hijacks the browser's "Save Page As"). Per-side string descriptor wins over umbrella. `controller.backdrop` / `controller.color` decouple the toolbar bg/icon-color from the modal `backdrop` (set both when `backdrop` is solid dark). `animate.cover` is preset-driven and defaults to `{ objectFit: true, clip: true, radius: true }`; it reads the cover `<img>` itself and does not infer parent-wrapper clipping. `clip-path` / `border-radius` animation may repaint; use `cover.clip=false` or `cover.radius=false` for performance-sensitive mobile pages. Set `animate={{ cover: false }}` for legacy cover geometry. `animate.slowMotion` defaults `false`; when enabled, holding `Shift` while opening or closing slows the full browsing transition to 10x for inspection and demos. `gesture` is preset-driven: desktop enables `wheelZoom` while already zoomed and disables `swipe` / `dragExit` / `pinchZoom` / `doubleTapZoom`; mobile enables horizontal drag paging, vertical drag-to-exit, pinch zoom, and double-tap zoom, while disabling `wheelZoom`. `gesture.touchAction` defaults to `'managed'`: pinch uses CSS `touch-action: none`, double-tap-only uses `manipulation`, otherwise `auto`; set it explicitly if the host page owns touch behavior. `pinchZoom.resetBelowFit` defaults `true`, so shrinking back to fit exits zoom and recenters. `doubleTapZoom.interval` / `distance` define the second-tap window. `wheelZoom.reverse` flips wheel direction; `wheelZoom.exitGuardDuration` defaults to `1000`, so wheel zooming out to `minScale` exits zoom immediately and blocks residual wheel events for that duration. `gesture=false` disables all gestures, and per-child overrides such as `gesture={{ swipe: false }}` / `gesture={{ wheelZoom: false }}` / `gesture={{ pinchZoom: false }}` keep the other preset defaults intact |
| Interface | `hideOnScroll`, `hideOnDblClick`, `coverVisible`, `backdrop`, `zIndex`, `radius`, `edge`, `loop`, `loadingDelay` | desktop-only flags noted in README. `radius` defaults to desktop `8` / mobile `0`; `edge` defaults to desktop `16` / mobile `0`. `hideOnScroll` and `hideOnDblClick` are the auto-dismiss trigger family (user action → close viewer); `hideOnDblClick` defaults `false`. `loadingDelay` defaults `200ms` — anti-flicker delay before showing the loading indicator (set 0 for legacy instant-show) |
| Interface | `hideOnScroll`, `hideOnDblClick`, `coverVisible`, `backdrop`, `zIndex`, `portalTarget`, `radius`, `edge`, `loop`, `loadingDelay` | desktop-only flags noted in README. `portalTarget` defaults to `document.body` and is for app overlay roots / modal roots / micro-frontend containers; it changes only the Portal mount parent, while the viewer remains fullscreen fixed. `radius` defaults to desktop `8` / mobile `0`; `edge` defaults to desktop `16` / mobile `0`. `hideOnScroll` and `hideOnDblClick` are the auto-dismiss trigger family (user action → close viewer); `hideOnDblClick` defaults `false`. `loadingDelay` defaults `200ms` — anti-flicker delay before showing the loading indicator (set 0 for legacy instant-show) |
| Lifecycle | `onBrowsing`, `onZooming`, `onSwitching`, `onRotating`, `onError` | first 4 callback args: `boolean`/`boolean`/`number`/`number`. `onError(e: SyntheticEvent<HTMLImageElement>)` fires for cover **or** viewer img-load failure — the only hook for the viewer-side failure (cover also flows via native `<img>` `onError` passthrough) |
| Native | All `HTMLAttributes<HTMLImageElement>` | className, style, onClick, etc. transparently forwarded to inner `<img>` |

Expand All @@ -102,7 +102,7 @@ Defaults & sub-shapes: see [`packages/core/src/types/default.ts`](./packages/cor
2. **Hard-coding `preset='desktop'` on a touch-targeted page** — omitted `preset` already defaults to `'auto'`. The desktop bundle ships hotkeys + arrow buttons, enables wheel zoom while zoomed, and disables mobile `gesture.swipe` / `gesture.dragExit` / `gesture.pinchZoom` / `gesture.doubleTapZoom`. Use `'desktop'` only when the page deliberately wants desktop behavior on touch devices.
3. **Treating `Zmage` as a class** — it is a `forwardRef` exotic component. ❌ `new Zmage()`. ✅ JSX or `Zmage.browsing()`.
4. **Mixing controlled and uncontrolled** — if `browsing` is in props, it must be a fully controlled `boolean` (provide `onBrowsing` to receive changes). Mixing both modes silently breaks state sync.
5. **Calling `Zmage.browsing` server-side** — it manipulates `document.body`. Guard with `typeof window !== 'undefined'` or call from event handlers / effects.
5. **Calling `Zmage.browsing` server-side** — it manipulates the DOM (`document.body` by default, or `portalTarget` when provided). Guard with `typeof window !== 'undefined'` or call from event handlers / effects.
6. **Putting `src` / `alt` on `<Zmage.Wrapper>` as if it rendered an image** — Wrapper binds real descendant `<img>` nodes. Put image data in the HTML, and pass only viewer config / optional shared `set` to Wrapper.
7. **Wrapping with `Zmage.Wrapper` without re-rendering** — wrapper attaches click handlers in `componentDidMount` / `componentDidUpdate` by querying `wrapperRef.current.querySelectorAll('img')`. Dynamically-injected `<img>` (after wrapper update) won't get attached unless wrapper re-renders.

Expand Down
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ The wrapper queries `<img>` descendants in `componentDidMount` / `componentDidUp
Wrapper-specific prop scope:

- Put `src` / `alt` on the child `<img>` nodes. Top-level `src` / `alt` are overwritten by the clicked DOM node.
- Viewer configuration still belongs on `<Zmage.Wrapper>`: `preset`, `controller`, `hotKey`, `animate`, `gesture`, `backdrop`, `zIndex`, `radius`, `edge`, `loop`, `coverVisible`, `hideOnScroll`, `hideOnDblClick`, `loadingDelay`, and lifecycle callbacks.
- Viewer configuration still belongs on `<Zmage.Wrapper>`: `preset`, `controller`, `hotKey`, `animate`, `gesture`, `backdrop`, `zIndex`, `portalTarget`, `radius`, `edge`, `loop`, `coverVisible`, `hideOnScroll`, `hideOnDblClick`, `loadingDelay`, and lifecycle callbacks.
- Pass `set` when the wrapped subtree should behave as one shared gallery. If the clicked image's `src` appears in `set`, Wrapper opens that matching index; `defaultPage` is only the fallback.
- Without `set`, the clicked image opens as a single image. `data-zmage-caption` or the nearest `figcaption` can provide the viewer caption.
- The controlled `browsing` prop is for component mode; it does not control `<Zmage.Wrapper>`.
Expand Down Expand Up @@ -537,11 +537,31 @@ Wheel zoom is active only while the viewer is already in zoom mode; normal brows
| `coverVisible` | `boolean` | `false` | Keep the cover `<img>` visible while the modal is open. |
| `backdrop` | `string` | `'#FFFFFF'` | Viewer backdrop. Any valid CSS color or gradient. **Default is white** — override (`'#111'`, etc.) for dark UIs. |
| `zIndex` | `number` | `1000` | Portal stacking. |
| `portalTarget` | `HTMLElement \| null` | `document.body` | Custom DOM element for mounting the viewer Portal. Use it when an app has a dedicated overlay root, modal root, shadow host, or micro-frontend container. It changes the mount parent only; the viewer still uses fullscreen fixed positioning. |
| `radius` | `number` | desktop `8`, mobile `0` | Image corner radius (px). |
| `edge` | `number` | desktop `16`, mobile `0` | Minimum margin between image and viewport (px). |
| `loop` | `boolean` | `true` | Wrap-around when paging past the ends. |
| `loadingDelay` | `number` | `200` | Delay (ms) before showing the loading indicator. If the image loads within this window, the indicator never appears — prevents the flash on cached page changes. Set 0 for legacy instant-show. |

`portalTarget` is for host apps that already centralize overlays outside the normal content tree. It does not make a local, clipped preview; use `zIndex` and your app shell's stacking rules to control how the fullscreen viewer sits above other UI.

```tsx
import { useState } from 'react'
import Zmage from 'react-zmage'
import 'react-zmage/style.css'

export function ArticleImage () {
const [viewerRoot, setViewerRoot] = useState<HTMLElement | null>(null)

return (
<section className="article-shell">
<div id="article-viewer-root" ref={setViewerRoot} />
<Zmage src="/photo.jpg" alt="Article photo" portalTarget={viewerRoot} />
</section>
)
}
```

### Lifecycle

| Prop | Signature | Triggered when |
Expand Down Expand Up @@ -569,7 +589,7 @@ export type BaseType =
& BaseParams // src / alt / caption / set / defaultPage
& PresetParams // preset
& FunctionalParams // controller / hotKey / animate / gesture
& InterfaceAndInteractionParams // hideOnScroll / hideOnDblClick / coverVisible / backdrop / zIndex / radius / edge / loop / loadingDelay
& InterfaceAndInteractionParams // hideOnScroll / hideOnDblClick / coverVisible / backdrop / zIndex / portalTarget / radius / edge / loop / loadingDelay
& LifeCycleParams // onBrowsing / onZooming / onSwitching / onRotating / onError
& ControlledParams // browsing
& HTMLAttributes<HTMLImageElement>
Expand Down
28 changes: 24 additions & 4 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function Trigger() {
包裹器模式下的参数范围:

- `src` / `alt` 应放在子级 `<img>` 上。顶层 `src` / `alt` 会被点击的 DOM 节点覆盖。
- 查看器配置仍然写在 `<Zmage.Wrapper>` 上:`preset`、`controller`、`hotKey`、`animate`、`gesture`、`backdrop`、`zIndex`、`radius`、`edge`、`loop`、`coverVisible`、`hideOnScroll`、`hideOnDblClick`、`loadingDelay` 和生命周期回调。
- 查看器配置仍然写在 `<Zmage.Wrapper>` 上:`preset`、`controller`、`hotKey`、`animate`、`gesture`、`backdrop`、`zIndex`、`portalTarget`、`radius`、`edge`、`loop`、`coverVisible`、`hideOnScroll`、`hideOnDblClick`、`loadingDelay` 和生命周期回调。
- 需要让包裹区内图片作为共享图库时传 `set`。若被点击图片的 `src` 出现在 `set` 中,Wrapper 会打开匹配索引;`defaultPage` 只作为兜底。
- 不传 `set` 时,被点击图片按单图打开。`data-zmage-caption` 或最近的 `figcaption` 可作为查看器 caption。
- 受控态 `browsing` 属于组件模式,不能控制 `<Zmage.Wrapper>`。
Expand Down Expand Up @@ -372,7 +372,7 @@ interface ControllerRenderSlots {
| `flip` | ✅ | — |
| `placement` | `top-right` | `top-right` |
| `radius` | `8` | `0` |
| `edge` | `30` | `0` |
| `edge` | `16` | `0` |
| `controller.layout.pagination.inset` | `24` | — |
| `controller.layout.caption.inset` | `60` | — |
| `gesture.swipe` | — | ✅ |
Expand Down Expand Up @@ -498,11 +498,31 @@ interface GestureDoubleTapZoomOptions {
| `coverVisible` | `boolean` | `false` | 放大期间是否保留封面图(默认会隐藏避免动画穿帮)。 |
| `backdrop` | `string` | `'#FFFFFF'` | 查看器背景色,接受任何合法 CSS color / gradient。**默认白色** —— 深色站点请显式覆盖(例如 `'#111'`)。 |
| `zIndex` | `number` | `1000` | Portal 容器的 `z-index`。 |
| `portalTarget` | `HTMLElement \| null` | `document.body` | 查看器 Portal 的自定义挂载目标。适合已有 overlay root、modal root、shadow host 或微前端容器的宿主应用。它只改变挂载父节点,查看器仍然使用 fixed 全屏布局。 |
| `radius` | `number` | desktop `8`,mobile `0` | 查看模式下图片圆角 (px)。 |
| `edge` | `number` | desktop `30`,mobile `0` | 图片距屏幕边缘的留白 (px)。 |
| `edge` | `number` | desktop `16`,mobile `0` | 图片距屏幕边缘的留白 (px)。 |
| `loop` | `boolean` | `true` | 多图模式:尾页是否循环回首页。 |
| `loadingDelay` | `number` | `200` | Loading 指示器显示前的延迟 (ms)。在此期间内图片加载完成则不显示 loading,避免快速切换缓存图时的视觉闪烁。默认 200ms (业界 react-loadable 经典值);设为 0 = 立即显示 (旧行为)。 |

`portalTarget` 用于宿主应用已经有统一弹层容器的场景。它不会把查看器裁剪成容器内的局部预览;需要调整遮罩层级时继续使用 `zIndex` 和宿主应用自己的层级规则。

```tsx
import { useState } from 'react'
import Zmage from 'react-zmage'
import 'react-zmage/style.css'

export function ArticleImage () {
const [viewerRoot, setViewerRoot] = useState<HTMLElement | null>(null)

return (
<section className="article-shell">
<div id="article-viewer-root" ref={setViewerRoot} />
<Zmage src="/photo.jpg" alt="文章图片" portalTarget={viewerRoot} />
</section>
)
}
```

### 生命周期

| 配置项 | 签名 | 触发时机 |
Expand Down Expand Up @@ -530,7 +550,7 @@ export type BaseType =
& BaseParams // src / alt / caption / set / defaultPage
& PresetParams // preset
& FunctionalParams // controller / hotKey / animate / gesture
& InterfaceAndInteractionParams // hideOnScroll / hideOnDblClick / coverVisible / backdrop / zIndex / radius / edge / loop / loadingDelay
& InterfaceAndInteractionParams // hideOnScroll / hideOnDblClick / coverVisible / backdrop / zIndex / portalTarget / radius / edge / loop / loadingDelay
& LifeCycleParams // onBrowsing / onZooming / onSwitching / onRotating / onError
& ControlledParams // browsing
& HTMLAttributes<HTMLImageElement>
Expand Down
4 changes: 2 additions & 2 deletions docs/404.html
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@
document.documentElement.classList.toggle('dark', theme === 'dark')
} catch {}
</script>
<script type="module" crossorigin src="/assets/index-Do1OB0YD.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DMpwH-Fy.css">
<script type="module" crossorigin src="/assets/index-C8ce7pKE.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-8Ptyfupf.css">
</head>
<body>
<div id="app">
Expand Down
Loading