nuxt-image-viewer 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ernest Manukyan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,261 @@
1
- # Temporary Holding Version
1
+ # nuxt-image-viewer
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![License][license-src]][license-href]
5
+ [![Nuxt][nuxt-src]][nuxt-href]
6
+
7
+ Full-screen image viewer (lightbox) for Nuxt. The image expands from its place on the page, scrolls with swipes, arrows and keys in an endless strip, zooms with a click, double tap, wheel or pinch, closes with a drag down — and flies back to its place. Springs, inertia and tuned gesture thresholds make it feel like a native photo app.
8
+
9
+ - No dependencies besides Nuxt — plain Vue, plain CSS, inline SVG icons
10
+ - Mark images with `<ZoomImage>` or a `data-image-viewer` attribute — no handlers in place
11
+ - Open from code with `useImageViewer()` — with or without thumbnails on the page
12
+ - Options in `nuxt.config` and per call, CSS variables, BEM classes, a cascade layer
13
+ - Dialog semantics, keyboard, focus trap and return, reduced motion
14
+ - Pointer events: the same gestures for mouse and touch, no empty frames at any swiping pace
15
+
16
+ ## Setup
17
+
18
+ ```bash
19
+ npx nuxt module add nuxt-image-viewer
20
+ ```
21
+
22
+ or install it yourself and add it to `modules`:
23
+
24
+ ```ts
25
+ // nuxt.config.ts
26
+ export default defineNuxtConfig({
27
+ modules: ['nuxt-image-viewer'],
28
+ imageViewer: {
29
+ // options, all optional — see below
30
+ },
31
+ })
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### `<ZoomImage>`
37
+
38
+ ```vue
39
+ <template>
40
+ <article data-image-viewer-group>
41
+ <ZoomImage src="/photos/a.jpg" :width="2400" :height="1600" caption="Dawn" />
42
+ <ZoomImage src="/photos/b.jpg" :width="1600" :height="2400" alt="Dusk" />
43
+ </article>
44
+ </template>
45
+ ```
46
+
47
+ | Prop | Type | |
48
+ | --- | --- | --- |
49
+ | `src` | `string` | Full-size image. |
50
+ | `width`, `height` | `number` | Natural size of `src` — the viewer fits and zooms by it. |
51
+ | `caption` | `string?` | Shown under the image in the viewer; also the default `alt`. |
52
+ | `alt` | `string?` | |
53
+
54
+ Other attributes (`class`, `loading`, `sizes`, …) pass through to the `<img>`. With [`@nuxt/image`](https://image.nuxt.com) installed the component renders `<NuxtImg>`: the page shows an optimized copy, the viewer the full image (see `nuxtImage`).
55
+
56
+ Click, Enter and Space open the viewer. It scrolls through the images of the closest `[data-image-viewer-group]`, without one — through every image of the page.
57
+
58
+ ### Plain `<img>`
59
+
60
+ The component only sets attributes — any image can be marked:
61
+
62
+ ```html
63
+ <img
64
+ src="/photos/a-small.jpg"
65
+ alt="Dawn"
66
+ tabindex="0"
67
+ data-image-viewer
68
+ data-image-viewer-src="/photos/a.jpg"
69
+ data-image-viewer-width="2400"
70
+ data-image-viewer-height="1600"
71
+ data-image-viewer-caption="Dawn over the ridges"
72
+ >
73
+ ```
74
+
75
+ | Attribute | |
76
+ | --- | --- |
77
+ | `data-image-viewer` | Opens the viewer. Only `<img>` elements. |
78
+ | `data-image-viewer-src` | Full image. Defaults to the image on the page (`currentSrc`). |
79
+ | `data-image-viewer-width` / `-height` | Natural size of the full image. Defaults to the page image's natural size. |
80
+ | `data-image-viewer-caption` | Caption. |
81
+ | `data-image-viewer-group` | On a container: scroll through its images only. |
82
+
83
+ ### `useImageViewer()`
84
+
85
+ Open the viewer from code with your own list of images. A page element for an image is optional: with `element` the image flies out of it and back, without one the viewer fades in and out.
86
+
87
+ ```ts
88
+ const viewer = useImageViewer()
89
+
90
+ viewer.open(
91
+ [
92
+ { src: '/photos/a.jpg', width: 2400, height: 1600, caption: 'Dawn' },
93
+ { src: '/photos/b.jpg', width: 1600, height: 2400, element: thumbRef.value },
94
+ ],
95
+ 1, // start index
96
+ { loop: false, bgOpacity: 1, ui: { fullscreen: false } }, // overrides for this call
97
+ )
98
+
99
+ watch(viewer.index, i => console.log('now at', i))
100
+ ```
101
+
102
+ | Member | Type | |
103
+ | --- | --- | --- |
104
+ | `open(items, index?, options?)` | `Promise<void>` | Open. `items` is an array or a single item. Ignored while open. |
105
+ | `close()` | `void` | Close with the animation. |
106
+ | `next()`, `prev()`, `goTo(index)` | `void` | Scroll. `goTo` takes the shortest way round the endless strip. |
107
+ | `isOpen` | `Readonly<Ref<boolean>>` | `true` from `open()` until closing starts. |
108
+ | `index` | `Readonly<Ref<number>>` | Current image. |
109
+ | `items` | `Readonly<Ref<ImageViewerSlide[]>>` | Images of the open viewer. |
110
+
111
+ `ImageViewerItem`:
112
+
113
+ | Field | Type | |
114
+ | --- | --- | --- |
115
+ | `src` | `string` | Full image. The only required field. |
116
+ | `width`, `height` | `number?` | Natural size. Without it the image is loaded to measure it first, so the viewer opens a bit later. |
117
+ | `thumb` | `string?` | Copy shown at once while `src` loads. Defaults to `element.currentSrc`, else `src`. |
118
+ | `alt`, `caption` | `string?` | |
119
+ | `element` | `HTMLElement?` | Page element to open from and close into. |
120
+
121
+ On the server `useImageViewer()` returns a no-op (`isOpen` is `false`). All types are exported: `import type { ImageViewerItem, ImageViewerOpenOptions } from 'nuxt-image-viewer'`.
122
+
123
+ ## Options
124
+
125
+ `nuxt.config` → `imageViewer`. Options marked *per call* can also be passed to `open()`.
126
+
127
+ | Option | Default | |
128
+ | --- | --- | --- |
129
+ | `zIndex` | `1000` | z-index of the viewer layer. *Per call.* |
130
+ | `bgOpacity` | `0.8` | Backdrop opacity, 0–1. *Per call.* |
131
+ | `loop` | `true` | Endless strip. Needs at least 3 images. *Per call.* |
132
+ | `spacing` | `0.1` | Gap between slides, share of the viewport width. *Per call.* |
133
+ | `maxZoom` | `4` | Zoom limit relative to the fitted image. Images zoom up to their natural size but at least 2×. *Per call.* |
134
+ | `closeOnVerticalDrag` | `true` | Close by dragging up or down. *Per call.* |
135
+ | `closeOnBackdropClick` | `true` | Close by a click outside the image. *Per call.* |
136
+ | `ui` | all `true` | `{ counter, zoom, fullscreen, close, arrows, caption }` — which controls to show. *Per call, partial.* |
137
+ | `labels` | English | `{ dialog, close, zoomIn, zoomOut, fullscreen, exitFullscreen, prev, next }` — accessible names. *Per call, partial.* |
138
+ | `openClass` | `'image-viewer-open'` | Class on `<html>` while open. |
139
+ | `hide` | `[]` | Selectors hidden while open, e.g. `['jdiv', '#intercom-container']`. |
140
+ | `autoOpen` | `true` | Open on click/Enter/Space on `img[data-image-viewer]`. `false` — only from code. |
141
+ | `componentName` | `'ZoomImage'` | Name of the image component. `false` — don't register it. |
142
+ | `nuxtImage` | `'auto'` | Render `<NuxtImg>` in the component. `'auto'` — when `@nuxt/image` is installed. |
143
+ | `css` | `true` | Include the default styles. |
144
+
145
+ Runtime options are also in `useRuntimeConfig().public.imageViewer`, so they can be overridden with `NUXT_PUBLIC_IMAGE_VIEWER_*` environment variables.
146
+
147
+ ## Styling
148
+
149
+ ### CSS variables
150
+
151
+ Set them on `:root`, on `.image-viewer` or on any ancestor.
152
+
153
+ | Variable | Default | |
154
+ | --- | --- | --- |
155
+ | `--image-viewer-bg` | `#000` | Backdrop colour (its opacity is `bgOpacity`). |
156
+ | `--image-viewer-color` | `#fff` | Text and icons. |
157
+ | `--image-viewer-font` | `inherit` | Font family. |
158
+ | `--image-viewer-counter-color` | `--image-viewer-color` | |
159
+ | `--image-viewer-button-bg` | `rgb(255 255 255 / 0.12)` | |
160
+ | `--image-viewer-button-bg-hover` | `rgb(255 255 255 / 0.22)` | Also on press. |
161
+ | `--image-viewer-button-color` | `--image-viewer-color` | |
162
+ | `--image-viewer-button-size` | `44px` | |
163
+ | `--image-viewer-button-radius` | `9999px` | |
164
+ | `--image-viewer-icon-size` | `24px` | |
165
+ | `--image-viewer-caption-bg` | `rgb(0 0 0 / 0.6)` | |
166
+ | `--image-viewer-caption-color` | `--image-viewer-color` | |
167
+ | `--image-viewer-caption-radius` | `8px` | |
168
+ | `--image-viewer-caption-font-size` | `14px` | |
169
+ | `--image-viewer-focus-color` | `#fff` in the viewer, `#2563eb` on page images | Focus ring. |
170
+
171
+ ```css
172
+ :root {
173
+ --image-viewer-bg: #fff;
174
+ --image-viewer-color: #111827;
175
+ --image-viewer-button-bg: rgb(0 0 0 / 0.06);
176
+ --image-viewer-button-radius: 12px;
177
+ }
178
+ ```
179
+
180
+ ### BEM classes
181
+
182
+ The default styles live in the `image-viewer` cascade layer, so any unlayered rule of yours wins without `!important`. The layer is declared after `base`, so Tailwind's preflight doesn't break the viewer images. Or turn them off with `css: false` and style from scratch:
183
+
184
+ | Class | |
185
+ | --- | --- |
186
+ | `.image-viewer` | Dialog layer, `100vw` × `100%`. Modifiers: `--zoomed`, `--closing`. |
187
+ | `.image-viewer__backdrop` | Backdrop; its opacity is set inline. |
188
+ | `.image-viewer__track` | Strip, moved by `transform`. |
189
+ | `.image-viewer__slide` | One slide. Modifier: `--current`. |
190
+ | `.image-viewer__frame` | Fitted image box; positioned inline. |
191
+ | `.image-viewer__zoom` | Zoomed and panned layer. |
192
+ | `.image-viewer__img` | Images. Modifiers: `--thumb` (page copy), `--full`, `--loaded`. |
193
+ | `.image-viewer__ui` | Controls layer. |
194
+ | `.image-viewer__toolbar` | Top bar. |
195
+ | `.image-viewer__counter` | `2 / 5`. |
196
+ | `.image-viewer__buttons` | Button group in the toolbar. |
197
+ | `.image-viewer__button` | Every button. Modifiers: `--zoom`, `--fullscreen`, `--close`. |
198
+ | `.image-viewer__arrow` | Prev / next (also `.image-viewer__button`). Modifiers: `--prev`, `--next`. Hidden on touch screens. |
199
+ | `.image-viewer__icon` | SVG icon, `stroke: currentColor`. |
200
+ | `.image-viewer__caption` | Caption container; `.image-viewer__caption-text` — its text. |
201
+
202
+ Page images get `cursor: zoom-in` and a focus ring via `[data-image-viewer]`.
203
+
204
+ ## Gestures and keys
205
+
206
+ | | Mouse | Touch | Keyboard |
207
+ | --- | --- | --- | --- |
208
+ | Next / previous | drag sideways, arrow buttons | swipe | `←` `→` |
209
+ | Zoom | click, wheel, zoom button | double tap, pinch | — |
210
+ | Pan when zoomed | drag | drag | — |
211
+ | Close | click outside, drag up/down, close button | tap outside, drag up/down | `Esc` |
212
+
213
+ ## Accessibility
214
+
215
+ - The viewer is a `role="dialog"` with `aria-modal` and a label; buttons have accessible names (`labels`).
216
+ - Focus moves into the dialog, `Tab` stays inside it, and on close focus returns to the page image (or to the element focused before `open()`).
217
+ - Page images are focusable (`tabindex="0"`) and open with Enter and Space; `alt` falls back to the caption.
218
+ - The counter is a polite live region; slides other than the current one are `aria-hidden`.
219
+ - With `prefers-reduced-motion: reduce` animations and springs are skipped.
220
+
221
+ ## How it works
222
+
223
+ - The page is not edited: on the first open the plugin imports the viewer component and renders it into a container in `<body>` with the app context, and unmounts it after closing.
224
+ - While open, page scroll is locked. If `<html>` has no `scrollbar-gutter: stable`, the scrollbar width is compensated with `padding-right` on `<body>`, so the page doesn't jump. Fixed elements of your own may still need the same.
225
+ - Every slide is in the DOM: while the full image loads, the copy already loaded on the page is shown, so there are no empty frames.
226
+ - While open, the page image of the current slide is `visibility: hidden` — the viewer image flies out of its place instead of doubling it.
227
+
228
+ ## For AI agents
229
+
230
+ - Mark images: `<ZoomImage src width height caption?>`; group with `data-image-viewer-group` on a container. Width/height are of the **full** image.
231
+ - Open from code: `useImageViewer().open(items, index?, options?)` — auto-imported, client-only, items need only `src`.
232
+ - Configure globally in `nuxt.config` → `imageViewer` (`ModuleOptions`); per call with `ImageViewerOpenOptions` (behaviour, `ui`, `labels`).
233
+ - Restyle with `--image-viewer-*` variables; deeper changes target `.image-viewer__*` classes (unlayered CSS wins), or `css: false`.
234
+ - Types: `ModuleOptions`, `ImageViewerItem`, `ImageViewerOpenOptions`, `ImageViewerApi`, `ImageViewerSlide`, `ImageViewerUi`, `ImageViewerLabels` from `'nuxt-image-viewer'`.
235
+
236
+ ## Development
237
+
238
+ ```bash
239
+ npm install
240
+ npm run dev:prepare # stub the module, generate types
241
+ npm run dev # playground on http://localhost:3000
242
+ npm run docs:dev # documentation site (Docus) in docs/
243
+ npm run docs:build # static documentation site in docs/.output/public
244
+ npm run lint
245
+ npm run test # e2e: SSR + browser (local: system Edge, CI: Chromium)
246
+ npm run test:types
247
+ ```
248
+
249
+ ## License
250
+
251
+ [MIT](./LICENSE)
252
+
253
+ <!-- Badges -->
254
+ [npm-version-src]: https://img.shields.io/npm/v/nuxt-image-viewer/latest.svg?style=flat&colorA=020420&colorB=00DC82
255
+ [npm-version-href]: https://npmjs.com/package/nuxt-image-viewer
256
+
257
+ [license-src]: https://img.shields.io/npm/l/nuxt-image-viewer.svg?style=flat&colorA=020420&colorB=00DC82
258
+ [license-href]: https://npmjs.com/package/nuxt-image-viewer
259
+
260
+ [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt
261
+ [nuxt-href]: https://nuxt.com
@@ -0,0 +1,31 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+ import { ImageViewerConfig } from '../dist/runtime/types.js';
3
+ export * from '../dist/runtime/types.js';
4
+
5
+ interface ModuleOptions extends ImageViewerConfig {
6
+ /**
7
+ * Name of the image component. `false` — don't register it and mark images with
8
+ * the `data-image-viewer*` attributes yourself.
9
+ * @default 'ZoomImage'
10
+ */
11
+ componentName: string | false;
12
+ /**
13
+ * Render `<NuxtImg>` inside the image component. `'auto'` — when `@nuxt/image` is installed.
14
+ * @default 'auto'
15
+ */
16
+ nuxtImage: boolean | 'auto';
17
+ /**
18
+ * Include the default styles. Turn off to style the BEM classes from scratch.
19
+ * @default true
20
+ */
21
+ css: boolean;
22
+ }
23
+ declare module '@nuxt/schema' {
24
+ interface PublicRuntimeConfig {
25
+ imageViewer: ImageViewerConfig;
26
+ }
27
+ }
28
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
29
+
30
+ export { _default as default };
31
+ export type { ModuleOptions };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nuxt-image-viewer",
3
+ "configKey": "imageViewer",
4
+ "compatibility": {
5
+ "nuxt": ">=3.13.0"
6
+ },
7
+ "version": "1.0.0",
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.3",
10
+ "unbuild": "3.6.1"
11
+ }
12
+ }
@@ -0,0 +1,61 @@
1
+ import { defineNuxtModule, createResolver, addPlugin, hasNuxtModule, addComponent, addImports } from '@nuxt/kit';
2
+ import { defu } from 'defu';
3
+
4
+ const module$1 = defineNuxtModule({
5
+ meta: {
6
+ name: "nuxt-image-viewer",
7
+ configKey: "imageViewer",
8
+ compatibility: { nuxt: ">=3.13.0" }
9
+ },
10
+ defaults: {
11
+ zIndex: 1e3,
12
+ bgOpacity: 0.8,
13
+ loop: true,
14
+ spacing: 0.1,
15
+ maxZoom: 4,
16
+ closeOnVerticalDrag: true,
17
+ closeOnBackdropClick: true,
18
+ ui: {
19
+ counter: true,
20
+ zoom: true,
21
+ fullscreen: true,
22
+ close: true,
23
+ arrows: true,
24
+ caption: true
25
+ },
26
+ labels: {
27
+ dialog: "Image viewer",
28
+ close: "Close",
29
+ zoomIn: "Zoom in",
30
+ zoomOut: "Zoom out",
31
+ fullscreen: "Fullscreen",
32
+ exitFullscreen: "Exit fullscreen",
33
+ prev: "Previous",
34
+ next: "Next"
35
+ },
36
+ openClass: "image-viewer-open",
37
+ hide: [],
38
+ autoOpen: true,
39
+ componentName: "ZoomImage",
40
+ nuxtImage: "auto",
41
+ css: true
42
+ },
43
+ setup(options, nuxt) {
44
+ const resolver = createResolver(import.meta.url);
45
+ const { componentName, nuxtImage, css, ...runtime } = options;
46
+ nuxt.options.runtimeConfig.public.imageViewer = defu(
47
+ nuxt.options.runtimeConfig.public.imageViewer,
48
+ runtime
49
+ );
50
+ if (css)
51
+ nuxt.options.css.push(resolver.resolve("./runtime/image-viewer.css"));
52
+ addPlugin({ src: resolver.resolve("./runtime/plugin.client"), mode: "client" });
53
+ if (componentName) {
54
+ const withNuxtImage = nuxtImage === "auto" ? hasNuxtModule("@nuxt/image", nuxt) : nuxtImage;
55
+ addComponent({ name: componentName, filePath: resolver.resolve(`./runtime/components/${withNuxtImage ? "ZoomNuxtImg" : "ZoomImage"}.vue`) });
56
+ }
57
+ addImports({ name: "useImageViewer", from: resolver.resolve("./runtime/composables/useImageViewer") });
58
+ }
59
+ });
60
+
61
+ export { module$1 as default };
@@ -0,0 +1,42 @@
1
+ import type { ImageViewerBehavior, ImageViewerSlide } from '../types.js';
2
+ /**
3
+ * Full-screen image viewer. Mounted by the plugin (plugin.client.ts) for one opening
4
+ * and unmounted after `closed` — it doesn't depend on Nuxt and gets everything in props.
5
+ *
6
+ * The strip is our own: every slide is in the DOM at once, so no pace of swiping shows
7
+ * an empty frame — while the full image loads, the copy already loaded on the page is
8
+ * visible. The image expands from its place on the page and collapses back into it.
9
+ *
10
+ * Motion is tuned to feel like a native photo app — the numbers below set the duration and
11
+ * curve of opening, closing and zoom, backdrop fade, strip and return springs, friction
12
+ * and gesture thresholds. Gestures run on pointer events, the same for mouse and finger:
13
+ * - drag sideways — scroll, down or up — close (the backdrop fades along);
14
+ * - pinch, wheel, mouse click or double tap — zoom, drag a zoomed image — pan;
15
+ * - click outside the image, Esc and the close button — close, arrows and ←/→ — scroll.
16
+ */
17
+ type __VLS_Props = {
18
+ items: ImageViewerSlide[];
19
+ /** Image to start from. */
20
+ index: number;
21
+ options: ImageViewerBehavior & {
22
+ openClass: string;
23
+ };
24
+ };
25
+ declare function goTo(i: number): void;
26
+ declare function close(): Promise<void>;
27
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
28
+ close: typeof close;
29
+ next: () => void;
30
+ prev: () => void;
31
+ goTo: typeof goTo;
32
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
33
+ change: (index: number) => any;
34
+ close: () => any;
35
+ closed: () => any;
36
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
37
+ onChange?: ((index: number) => any) | undefined;
38
+ onClose?: (() => any) | undefined;
39
+ onClosed?: (() => any) | undefined;
40
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
41
+ declare const _default: typeof __VLS_export;
42
+ export default _default;