npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

right-image-preview

v0.7.0

Published

A dependency-free React image preview component with Lightroom-style discrete zoom stops, multi-group navigation, flip/rotate, auto-fade controls, and full keyboard support.

Readme

right-image-preview

English · 中文

在线演示

👉 https://zhangjian1713.github.io/right-image-preview/

在浏览器中打开交互式演示(右上角可切换 EN / 中文),无需本地安装。

无 UI 库依赖的 React 媒体预览组件。v0.4 的静态位图主舞台使用 WebGL2;SVG、动画图与 Video 使用各自的原生浏览器模块,并共享同一套 Viewer Shell。

本项目是公开、开源、免费的 MIT 通用组件,正式版本发布到 npm;任何应用专属的商业逻辑、付费能力或私有产品代码都不属于本仓库。


✨ 特性

| 能力 | 说明 | |------|------| | WebGL2 静态位图 | JPEG、静态 PNG/WebP、AVIF 等主图统一使用 canvas + GPU texture;不维护第二套完整 DOM Raster renderer | | Raster 安全回退 | WebGL2、能力、纹理或 context 失败时保留同一 Viewer Shell,以原生 DOM renderer 显示当前静态图;context 恢复后切回 WebGL2 | | 多媒体分发 | SVG、GIF/APNG/Animated WebP、Video、unknown 各自独立模块;工具栏按 capability 自动禁用不支持的操作 | | 多宿主图源 | 导出 MediaSource,支持 URL、Blob、ArrayBuffer bytes;可由浏览器、VS Code Webview、Tauri 宿主提供 | | Fit / Native 双模式 | fit 以 contain 语义完整显示图片;native 以原始像素为 100% 基准 | | 固定档位缩放 | 放大/缩小只在离散档位间跳转(默认 5 %–200 %);可用 stops 自定义 | | 缩放输入 | 可输入正整数 %;工具栏提交值会限制在最大档位ref 调用不限制) | | 多图 / 多组导航 | 支持单组图片列表,也支持按文件夹/分组组织的多组图片 | | 翻转 & 旋转 | 水平/垂直翻转,90° 顺/逆时针旋转,带 CSS 动画 | | 缩放锁定 | 切换图片时可选择保留或重置缩放状态 | | 智能侧边箭头 | 不可导航时箭头完全隐藏;组边界自动变为跳组按钮(双箭头) | | 控件自动渐隐 | 3 秒无操作后控件渐隐至约 10% 透明度,任意活动立即恢复 | | 导航小地图 | 主图溢出视口时右下角缩略图 + 可拖视口框;可通过 showMinimap 关闭 | | 缩略图条 | showThumbnails 开启底部横向缩略图(默认关)。thumbnailsScope="group"(默认)分组时仅当前组;"flat" 为整段扁平序列(过长时窗口虚拟化);桌面宿主可用 onThumbnailVisibleIndexesChange 只生成可见窗口 | | 嵌入式模式 | presentation="contained" 填满宿主容器;仅聚焦时响应键盘 | | Browse/Screen/Full 分档 | 邻图数量按实时图片舞台 DIV × DPR 与真实纹理预算动态计算;若全部候选的 Screen 都能放下则全部使用 Screen,否则连续核心会随压力在前后各 1 张至前 3 / 后 2 张之间动态变化,核心外从近到远连续铺固定 Browse 环。当前张停稳 300ms 后才升级 Full | | 无队列长按导航 | ←/→ 第一张立即切换;holdRepeatDelayMs 控制进入连续切换的门槛,holdMinVisibleMs 控制每张真正呈现后的最短展示时间;松开立即停止 | | 控件密度 | chrome="minimal" 空闲时控件完全隐藏 | | 宿主优先全屏 | 可选 fullscreen 适配器接管状态和进出;未提供时回退标准浏览器 Fullscreen API。Esc 优先退出全屏 | | 受控下标 | index + onIndexChange;ref goTo(index) | | 小地图独立图源 | 每条 ImageItem 可设 minimapSource / minimap;默认复用主预览或原图 | | 触控双指捏合缩放 | 双指捏合/展开实现连续缩放;缩放锚点跟随双指中点;可用 pinchEnabled 关闭 | | 界面语言 | language 内置 英文简体中文enzhzh-CN 等);可用 strings 覆盖任意文案 | | 丰富的键盘快捷键 | Esc / ±方向键 / Space / PageUp-Down / Ctrl+方向键平移 / Shift+方向键平移(或旋转) | | 可访问性 | Overlay:role="dialog" + aria-modal 与焦点管理;presentation="contained"role="region"(非整页模态),仅预览聚焦时响应键盘。控件带 aria-label | | TypeScript 一等类型 | 完整类型导出,forwardRef 支持命令式 ref API | | 零生产依赖 | 仅依赖 React,无任何第三方 UI 库 | | 兼容性:React 17+ | 兼容 react / react-dom ≥ 17,推荐 18+(滚轮多档时原生 flushSync 体验最佳) |


快速开始

在业务项目中使用

npm install right-image-preview

克隆本仓库(演示页、测试、参与开发)

npm install
npm run dev       # 浏览器打开 http://localhost:5173
npm test
npm run build     # 演示站的 Vite 生产构建

浏览器访问 http://localhost:5173/right-image-preview/,页面右上角可切换 EN / 中文

  • Demo 0:触发器模式,单个子节点打开预览(非受控)
  • Demo 1:单组相册,点击遮罩关闭,无翻转按钮
  • Demo 2:多文件夹分组,侧边箭头,含翻转按钮
  • Demo 3:先小图占位、再载入全图(Wikimedia Commons;WebGL 用 fetch,需要 CORS)
  • Demo 4:宿主 EXIF + 删除
  • Demo 5:嵌入式工作区 + 扁平缩略图条 + 相邻预加载 + minimal chrome
  • Demo 6:预加载实验室。默认 Wikimedia(约 1280–1920px,带 CORS);npm run dev 时可切到本地 ./test-images

基本用法

import { ImagePreview } from 'right-image-preview';

// 单张图片
<ImagePreview
  source={{ type: 'url', href: '/photo.jpg' }}
  visible={open}
  onClose={() => setOpen(false)}
/>

// Blob / bytes / Webview URI / Tauri asset URL
<ImagePreview
  source={{ type: 'blob', blob: photoBlob }}
  kind="raster"
  visible={open}
/>

// 多图列表
<ImagePreview
  images={[
    { source: { type: 'url', href: '/a.jpg' }, name: 'a.jpg' },
    { source: { type: 'url', href: '/b.jpg' }, name: 'b.jpg' },
  ]}
  visible={open}
  onClose={() => setOpen(false)}
  wheelEnabled
  doubleClickEnabled
  closeOnMaskClick
/>

// 多组(文件夹)图片
<ImagePreview
  groupedImages={[
    {
      name: '旅行/',
      images: [
        { id: 'travel/a', source: { type: 'url', href: '/a.jpg' }, name: 'a.jpg' },
        { source: { type: 'url', href: '/b.jpg' }, name: 'b.jpg' },
        { source: { type: 'url', href: '/c.jpg' }, name: 'c.jpg' },
      ],
    },
    {
      name: '活动/',
      images: [
        { source: { type: 'url', href: '/d.jpg' }, name: 'd.jpg' },
        { source: { type: 'url', href: '/e.jpg' }, name: 'e.jpg' },
        { source: { type: 'url', href: '/f.jpg' }, name: 'f.jpg' },
      ],
    },
  ]}
  visible={open}
  onClose={() => setOpen(false)}
  arrows="side"
  showFlip
/>

渲染边界、context recovery、纹理缓存和宿主接入见渲染架构


API

完整权威说明: docs/api.zh-CN.md(中文)/ docs/api.md(英文)。切图主流程(先缩略再原图):docs/main-display-flow.zh-CN.md。下表仅为速览,若有出入以 docs/api 为准。

Props

| Prop | 类型 | 默认值 | 说明 | |------|------|--------|------| | source | MediaSource | — | 可选原图 URL / Blob / bytes | | previewSource | MediaSource | — | 宿主提供的有界主预览;原图未知或不安全时先显示它 | | naturalSize | { width, height } | — | 原图经方向校正后的自然尺寸 | | sourceRevision / previewRevision | string \| number | — | 同一资源内容或预览质量变化时更新 | | kind | MediaKind | 自动识别 | 可明确指定 raster / svg / animated-image / video / unknown | | minimapSource | MediaSource | — | 导航小地图专用图源;默认复用 preview/original | | minimap | React.ReactNode | — | 自定义小地图(覆盖 minimapSource) | | images | ImageItem[] | — | 扁平多图列表 | | groupedImages | ImageGroup[] | — | 文件夹式分组;优先于 images 和单媒体字段 | | visible | boolean | true | 控制预览显示/隐藏 | | defaultGroupedSelection | { defaultGroupIndex, defaultIndexInGroup } | — | groupedImages 模式下的初始图(组下标只计非空组);优先于 defaultIndex | | defaultIndex | number | 0 | 扁平列表中的初始下标;与分组同时传入 defaultGroupedSelection 时忽略 | | stops | number[] | [5,10,20,35,50,75,100,125,150,175,200] | Native zoom 档位(%,升序);需要更高上限请传入更长列表 | | initialMode | 'fit' \| 'native' | 'fit' | 初始缩放模式 | | initialNativePercent | number | 第一档 | initialMode='native' 时的初始比例 | | fitMaxNativePercent | number | 无上限 | Fit / contain 的上限(按 native %)。100 表示小图保持 1:1,不铺满窗口;省略则允许 CSS contain 放大 | | fullscreen | FullscreenAdapter | — | 宿主拥有的全屏状态与进出动作;优先于浏览器 Fullscreen API | | onFullscreenError | (error: unknown) => void | — | 浏览器 API 缺失/拒绝/状态未确认及宿主适配器失败的错误回调 | | shiftArrowAction | 'pan' \| 'rotate' | 'pan' | Shift + 方向键默认平移;'rotate' 时左右键仍为 90° 旋转 | | rasterDecodeWorkers | number \| 'auto' | 'auto' | Raster 专用解码 Worker 数;auto 按逻辑 CPU 并发保守选择 1–3 | | rasterDecodeWorkerMax | number | 3 | 解码并发安全上限;超过 8000 万像素的来源仍独占解码 | | rasterFullDecodeMaxBytes | number | 1 GiB | 原图 Full decode 的预估自然 RGBA8 上限;超限已知尺寸图使用 previewSource | | onRasterRendererStateChange | (state: RasterRendererState) => void | — | 回报 WebGL2/DOM renderer、能力值、fallback 原因及 context lost/restored 状态 | | onThumbnailVisibleIndexesChange | (indexes: number[]) => void | — | 底片虚拟列表实际挂载下标;宿主可只生成这批缩略图 | | firstZoomInStrategy | 'above-fit' \| 'first-stop' \| 'hundred' | 'above-fit' | 从 Fit 首次放大时的入档策略 | | zoomOutBelowMinBehaviour | 'fit' \| 'noop' | 'noop' | 缩小到最小档以下的行为 | | zoomInAtMaxBehaviour | 'noop' \| 'notify' | 'noop' | 放大到最大档时的行为 | | wheelEnabled | boolean | true | 是否启用滚轮缩放 | | doubleClickEnabled | boolean | true | 双击切换 Fit ↔ 100% | | switchImageResetZoom | boolean | true | 切图时是否重置缩放(锁定时被覆盖) | | switchImageResetTransform | boolean | true | 切图时是否重置翻转/旋转 | | fitResetPan | boolean | true | 切回 Fit 时是否归零平移 | | showFlip | boolean | false | 是否显示翻转按钮 | | showExif | boolean | false | 是否在工具栏显示 EXIF / 元数据开关 | | initialExifOpen | boolean | false | 开启 showExif 时是否默认展开 EXIF 面板 | | showDelete | boolean | false | 是否显示删除按钮;宿主在 onDeleteImage 中更新列表 | | arrows | 'both' \| 'side' \| 'toolbar' \| 'none' | 'both' | 仅控制两侧箭头;非空 groupedImages 时工具栏上一张/下一张始终显示 | | initialZoomLocked | boolean | false | 初始是否锁定缩放 | | closeOnMaskClick | boolean | false | 点击遮罩区域是否关闭预览 | | onClose | () => void | — | 关闭回调 | | onZoomChange | (state: ZoomState) => void | — | 缩放状态变化回调 | | onIndexChange | (index: number) => void | — | 图片索引变化回调 | | onMaxStopReached | () => void | — | 到达最大档位回调(需配合 'notify') | | onImageError | (index: number, item: ImageItem) => void | — | 当前媒体失败时回调 | | errorFallback | (index: number, item: ImageItem) => ReactNode | — | 当前媒体失败时的自定义占位 | | pinchEnabled | boolean | true | 是否启用双指捏合缩放(触屏 / 多点触控板) | | strings | Partial<LocaleStrings> | — | 覆盖任意 UI 文案字段;叠加在 language 所选语言之上;只需提供要改的字段 |

arrows 取值说明

| 值 | 效果 | |----|------| | 'both' | 两侧箭头 + 扁平列表时工具栏上一张/下一张(默认) | | 'side' | 仅两侧箭头;扁平列表时工具栏仍有上一张/下一张,序号在中间 | | 'toolbar' | 仅工具栏上一张/下一张;无两侧箭头 | | 'none' | 无两侧箭头;键盘 ← → 仍可用;扁平列表时工具栏仍有上一张/下一张与序号 |

传入非空 groupedImages 时,工具栏上一张/下一张始终显示;本表只约束两侧箭头。

类型定义

interface ImageItem {
  id?: string;   // 稳定主键(如路径);身份识别请优先于 `name`
  source?: MediaSource;
  sourceRevision?: string | number;
  previewSource?: MediaSource;
  previewRevision?: string | number;
  naturalSize?: { width: number; height: number };
  alt?: string;
  name?: string; // 工具栏信息栏显示的文件名
  minimapSource?: MediaSource;
  thumbnailSource?: MediaSource | null; // null 时不回退原图
  minimap?: React.ReactNode;
}

interface ImageGroup {
  id?: string;   // 可选稳定主键(如目录路径)
  name: string;  // 组名,显示在文件名下方
  images: ImageItem[];
}

interface DefaultGroupedSelection {
  defaultGroupIndex: number;   // 仅统计非空组,顺序同 `groupedImages`
  defaultIndexInGroup: number; // 在该组 `images` 内的 0-based 下标
}

interface ZoomState {
  mode: 'fit' | 'native';
  nativePercent: number;
  fitEquivalentNativePercent?: number; // 供 UI 显示"适应 ≈ xx%"
}

interface FullscreenAdapter {
  isFullscreen: boolean;
  enter(): void | Promise<void>;
  exit(): void | Promise<void>;
}

传入 fullscreen 后,它是全屏状态的唯一来源:工具栏、Esc 与 ref 方法都只委托给适配器,不读取或调用 DOM Fullscreen API。未传时组件按能力检测使用标准浏览器 API,只有 fullscreenchange 确认目标状态后才更新工具栏。

包内还导出 resolvePreviewImagesflattenGroupedImagesresolveDefaultGroupedFlatIndexFlattenedGroupSliceDefaultGroupedSelection,便于在组件外复用相同的扁平列表与组内下标范围。resolveStringsmergeStrings 也一并导出,可用于在组件外以编程方式构建自定义 locale 对象。

Ref API

const ref = useRef<ImagePreviewRef>(null);

interface ImagePreviewRef {
  // 缩放
  zoomIn(): void;
  zoomOut(): void;
  fit(): void;
  setNative(percent: number): void; // 任意正数(不截断;工具栏输入会限制在最大档位)

  // 变换
  rotateCW(): void;
  rotateCCW(): void;
  flipHorizontal(): void;
  flipVertical(): void;

  // 导航
  next(): void;
  prev(): void;
  nextGroup(): void;
  prevGroup(): void;

  // 全屏(传入宿主适配器时委托适配器,否则走浏览器兜底)
  requestFullscreen(): Promise<boolean>;
  exitFullscreen(): Promise<void>;
  isFullscreen(): boolean;

  // 状态读取
  getState(): ZoomState;
}

键盘快捷键

| 按键 | 行为 | |------|------| | Esc | 关闭预览 | | + / = / | 放大一档 | | - / | 缩小一档 | | 0 | 适应视口(Fit) | | 1 | 原图 100% | | Space | 切换 Fit ↔ 100% | | / | 上一张 / 下一张 | | Ctrl/⌘ + 方向键 | 向对应方向平移视野(步长为视口较短边的 15%) | | Shift + 方向键 | 由 shiftArrowAction 决定,默认平移;设为 rotate 时左右键旋转 | | PageUp | 跳到上一组第一张(需非空 groupedImages) | | PageDown | 跳到下一组第一张(需非空 groupedImages) |

任意按键操作均会重置控件自动渐隐计时器。


项目结构

src/
  components/ImagePreview/
    types.ts                   # TypeScript 类型定义
    flattenGroupedImages.ts    # resolvePreviewImages / flattenGroupedImages 辅助函数
    useZoomState.ts            # 缩放状态机 Hook(纯逻辑,无 DOM)
    useImageTransform.ts       # 尺寸测量 + CSS transform 计算 + 拖拽平移
    useWheelZoom.ts            # 滚轮缩放 Hook
    usePinchZoom.ts            # 触控双指捏合缩放 Hook
    injectGlobalStyle.ts       # 单例 CSS 注入工具(SSR 安全)
    locale.ts / localeTypes.ts / locales/  # 国际化字符串
    Toolbar.tsx                # 底部工具栏
    Minimap.tsx                # 导航小地图
    shell/
      ImagePreview.tsx         # 外层 shell(trigger 模式分发)
      ImagePreviewInner.tsx    # 对话框实现
      ImagePreviewTriggerShell.tsx
    parts/
      ImagePreviewCloseButton.tsx
      ImagePreviewNavArrow.tsx
    index.ts                   # 公开导出
  App.tsx                      # 演示页外壳
  demos/                       # 各 Demo 与演示站文案(不打进 npm 包)
test-images/                   # gitignored 本地图,仅 Demo 6(`npm run dev`)
scripts/
  localTestImages.ts           # Demo 6 递归列目录 / 按需缩略图
  generate-test-image-thumbs.sh
docs/
  main-display-flow.zh-CN.md / .md   # 主图显示与切图主流程(先缩略再原图)
  api.md / api.zh-CN.md              # Props & Ref API 参考
  media-lens-integration.zh-CN.md    # Media Lens(Tauri)接入清单
  media-lens-integration.md          # English checklist
  keyboard.md / keyboard.zh-CN.md   # 键盘快捷键说明
  requirements.md                    # 需求迭代记录
tests/
  setup.ts                     # Vitest + jsdom 配置
  useZoomState.test.ts         # 缩放状态机单元测试
  flattenGroupedImages.test.ts
  locale.test.ts               # Locale / mergeStrings 单元测试
  injectGlobalStyle.test.ts
  ImagePreview.test.tsx        # 组件集成测试

缩放算法

fitScale   = min(containerW / naturalW, containerH / naturalH, fitMaxScale)
             fitMaxScale = fitMaxNativePercent / 100(省略则为 ∞,即 CSS contain 可放大)
nativeScale = nativePercent / 100

CSS transform scale(fit)    = fitScale
CSS transform scale(native) = nativeScale × (naturalW / layoutW)
                              ≈ nativePercent / 100

fitEquivalentNativePercent    = fitScale × 100(供 UI 显示"适应 ≈ xx%")

后续迭代方向

  • 旋转 90°/270° 时的严格 1:1 约束(宽高调换)
  • 弹簧物理动画(缩放/平移更自然的惯性)

版本号约定(1.0 之前)

当前为 0.x 阶段,版本大致按下面划分:

  • Patch(修订号) — 仅影响内置文案或类型上的扩展(例如在 LocaleStrings / locales/*.ts 中新增字段、改 tooltip 措辞)、未导出组件的内部 props 调整、缺陷修复,以及不改变 ImagePreview 已文档化的对外契约(props、ref、按文档使用的导出函数与类型)的改动。
  • Minor(次版本) — 新增或变更已文档化的 props、ref 能力、导出 API,或集成方需要跟进的可见行为变化。

若你在 TypeScript 里手写整份 LocaleStrings,升级后可能需要补全新增字段;本项目把这种情形仍算作 patch,因为常见用法是 language + 内置 resolveStrings


License

MIT © ZhangJian