react-photoswipe-autosize 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 +21 -0
- package/README.md +118 -0
- package/dist/index.cjs +170 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +141 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +50 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# react-photoswipe-autosize
|
|
2
|
+
|
|
3
|
+
Drop-in auto-sizing for [react-photoswipe-gallery](https://github.com/dromru/react-photoswipe-gallery) — **no `width`/`height` props needed**.
|
|
4
|
+
|
|
5
|
+
Image dimensions are detected automatically at runtime using background preloading, with a material-style loading spinner shown during detection.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Zero-config dimensions** — just provide `original` and `thumbnail`, no width/height
|
|
10
|
+
- **Instant cached opens** — dimensions are cached across gallery opens; second click is instant
|
|
11
|
+
- **Hover pre-caching** — optional `usePreloadOnHover()` pre-fetches dimensions on mouse enter
|
|
12
|
+
- **Loading spinner** — Material arc spinner while dimensions are detected
|
|
13
|
+
- **No blur flash** — disables PhotoSwipe's default blurry thumbnail-to-image transition
|
|
14
|
+
- **Race-safe** — handles rapid navigation, in-flight deduplication, and stale slide recycling
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install react-photoswipe-autosize photoswipe react-photoswipe-gallery
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { AutoSizeGallery, AutoSizeItem, usePreloadOnHover } from 'react-photoswipe-autosize'
|
|
26
|
+
import 'react-photoswipe-autosize/styles.css'
|
|
27
|
+
import 'photoswipe/dist/photoswipe.css'
|
|
28
|
+
|
|
29
|
+
function MyGallery() {
|
|
30
|
+
const preload = usePreloadOnHover()
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<AutoSizeGallery>
|
|
34
|
+
<AutoSizeItem original="https://example.com/big.jpg" thumbnail="https://example.com/thumb.jpg">
|
|
35
|
+
{({ ref, open }) => (
|
|
36
|
+
<img
|
|
37
|
+
ref={ref}
|
|
38
|
+
onClick={open}
|
|
39
|
+
onMouseEnter={() => preload('https://example.com/big.jpg')}
|
|
40
|
+
src="https://example.com/thumb.jpg"
|
|
41
|
+
alt="My photo"
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
44
|
+
</AutoSizeItem>
|
|
45
|
+
</AutoSizeGallery>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
### `<AutoSizeGallery>`
|
|
53
|
+
|
|
54
|
+
Drop-in replacement for `<Gallery>`. Accepts all the same props plus auto-size behavior.
|
|
55
|
+
|
|
56
|
+
| Prop | Type | Description |
|
|
57
|
+
|------|------|-------------|
|
|
58
|
+
| `options` | `PhotoSwipeOptions` | PhotoSwipe options (auto-size defaults are applied first, your overrides win) |
|
|
59
|
+
| `onBeforeOpen` | `(pswp) => void` | Called after auto-size hooks are registered |
|
|
60
|
+
|
|
61
|
+
Default options applied:
|
|
62
|
+
- `showHideAnimationType: 'none'`
|
|
63
|
+
- `bgOpacity: 1`
|
|
64
|
+
- Animation durations: `0`
|
|
65
|
+
|
|
66
|
+
### `<AutoSizeItem>`
|
|
67
|
+
|
|
68
|
+
Drop-in replacement for `<Item>`. Same props but `width`/`height` are **optional** (defaults to auto-detection).
|
|
69
|
+
|
|
70
|
+
| Prop | Type | Description |
|
|
71
|
+
|------|------|-------------|
|
|
72
|
+
| `original` | `string` | Full-size image URL |
|
|
73
|
+
| `thumbnail` | `string` | Thumbnail URL |
|
|
74
|
+
| `width` | `number \| string` | Optional override (bypasses auto-detection) |
|
|
75
|
+
| `height` | `number \| string` | Optional override (bypasses auto-detection) |
|
|
76
|
+
|
|
77
|
+
### `usePreloadOnHover()`
|
|
78
|
+
|
|
79
|
+
Returns a `(src: string) => void` callback for pre-caching dimensions on hover.
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
const preload = usePreloadOnHover()
|
|
83
|
+
<img onMouseEnter={() => preload(fullSizeUrl)} />
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Theming
|
|
87
|
+
|
|
88
|
+
The spinner color automatically matches PhotoSwipe's `--pswp-icon-color` variable. Override it:
|
|
89
|
+
|
|
90
|
+
```css
|
|
91
|
+
/* Custom spinner color */
|
|
92
|
+
.pswp {
|
|
93
|
+
--pswp-spinner-color: #ff0000;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Light theme example */
|
|
97
|
+
.pswp {
|
|
98
|
+
--pswp-bg: #fff;
|
|
99
|
+
--pswp-icon-color: #000;
|
|
100
|
+
--pswp-icon-color-secondary: #bbb;
|
|
101
|
+
--pswp-icon-stroke-color: #bbb;
|
|
102
|
+
}
|
|
103
|
+
.pswp__counter { color: #000; text-shadow: none; }
|
|
104
|
+
.pswp__top-bar { background: none; }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## How it works
|
|
108
|
+
|
|
109
|
+
1. `<AutoSizeItem>` renders with `1×1` placeholder dimensions
|
|
110
|
+
2. On lightbox open, an `itemData` filter injects any cached dimensions before slide creation
|
|
111
|
+
3. For uncached images, `contentLoad` fires a background `new Image()` preload
|
|
112
|
+
4. `contentAppend` hides the placeholder element and shows a spinner
|
|
113
|
+
5. Once dimensions resolve, `refreshSlideContent` recreates the slide at full size
|
|
114
|
+
6. A deduplication layer (`startPreload`) prevents duplicate loads from hover + click
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AutoSizeGallery: () => AutoSizeGallery,
|
|
24
|
+
AutoSizeItem: () => AutoSizeItem,
|
|
25
|
+
usePreloadOnHover: () => usePreloadOnHover
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/AutoSizeGallery.tsx
|
|
30
|
+
var import_react = require("react");
|
|
31
|
+
var import_react_photoswipe_gallery = require("react-photoswipe-gallery");
|
|
32
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
33
|
+
var SPINNER_SVG = `<svg class="pswp-spinner" viewBox="0 0 50 50">
|
|
34
|
+
<circle class="pswp-spinner__path" cx="25" cy="25" r="20"
|
|
35
|
+
fill="none" stroke-width="5"></circle>
|
|
36
|
+
</svg>`;
|
|
37
|
+
var dimensionCache = /* @__PURE__ */ new Map();
|
|
38
|
+
function preloadImage(src) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const cached = dimensionCache.get(src);
|
|
41
|
+
if (cached) {
|
|
42
|
+
resolve(cached);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const img = new Image();
|
|
46
|
+
img.onload = () => {
|
|
47
|
+
const dims = { w: img.naturalWidth, h: img.naturalHeight };
|
|
48
|
+
dimensionCache.set(src, dims);
|
|
49
|
+
resolve(dims);
|
|
50
|
+
};
|
|
51
|
+
img.onerror = () => {
|
|
52
|
+
const dims = { w: 800, h: 600 };
|
|
53
|
+
resolve(dims);
|
|
54
|
+
};
|
|
55
|
+
img.src = src;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
var inFlightPreloads = /* @__PURE__ */ new Map();
|
|
59
|
+
function startPreload(src) {
|
|
60
|
+
const cached = dimensionCache.get(src);
|
|
61
|
+
if (cached) return Promise.resolve(cached);
|
|
62
|
+
const existing = inFlightPreloads.get(src);
|
|
63
|
+
if (existing) return existing;
|
|
64
|
+
const promise = preloadImage(src).then((dims) => {
|
|
65
|
+
inFlightPreloads.delete(src);
|
|
66
|
+
return dims;
|
|
67
|
+
});
|
|
68
|
+
inFlightPreloads.set(src, promise);
|
|
69
|
+
return promise;
|
|
70
|
+
}
|
|
71
|
+
function usePreloadOnHover() {
|
|
72
|
+
return (0, import_react.useCallback)((src) => {
|
|
73
|
+
startPreload(src);
|
|
74
|
+
}, []);
|
|
75
|
+
}
|
|
76
|
+
function AutoSizeGallery({
|
|
77
|
+
children,
|
|
78
|
+
options,
|
|
79
|
+
onBeforeOpen: userOnBeforeOpen,
|
|
80
|
+
...rest
|
|
81
|
+
}) {
|
|
82
|
+
const handleBeforeOpen = (pswp) => {
|
|
83
|
+
pswp.addFilter("itemData", (itemData) => {
|
|
84
|
+
const src = itemData?.src;
|
|
85
|
+
if (!src) return itemData;
|
|
86
|
+
const cached = dimensionCache.get(src);
|
|
87
|
+
if (cached) {
|
|
88
|
+
itemData.width = cached.w;
|
|
89
|
+
itemData.height = cached.h;
|
|
90
|
+
itemData.w = cached.w;
|
|
91
|
+
itemData.h = cached.h;
|
|
92
|
+
}
|
|
93
|
+
return itemData;
|
|
94
|
+
});
|
|
95
|
+
pswp.addFilter("useContentPlaceholder", () => false);
|
|
96
|
+
pswp.on("contentLoad", (e) => {
|
|
97
|
+
const { content } = e;
|
|
98
|
+
const src = content?.data?.src;
|
|
99
|
+
if (!src) return;
|
|
100
|
+
if (dimensionCache.has(src)) return;
|
|
101
|
+
startPreload(src).then(() => {
|
|
102
|
+
try {
|
|
103
|
+
const idx = pswp.getNumItems?.() ? Array.from({ length: pswp.getNumItems() }, (_, i) => i).find((i) => {
|
|
104
|
+
const data = pswp.getItemData?.(i);
|
|
105
|
+
return data?.src === src;
|
|
106
|
+
}) : content.slide?.index;
|
|
107
|
+
if (idx !== void 0 && idx !== -1 && pswp.currSlide) {
|
|
108
|
+
pswp.refreshSlideContent(idx);
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
pswp.on("contentAppend", (e) => {
|
|
115
|
+
const { content } = e;
|
|
116
|
+
const src = content?.data?.src;
|
|
117
|
+
if (!src) return;
|
|
118
|
+
const imgEl = content.element;
|
|
119
|
+
if (imgEl?.complete && imgEl.naturalWidth > 1) return;
|
|
120
|
+
if (imgEl) {
|
|
121
|
+
imgEl.style.visibility = "hidden";
|
|
122
|
+
const reveal = () => {
|
|
123
|
+
imgEl.style.visibility = "";
|
|
124
|
+
const spinner = content.slide?.container?.querySelector(".pswp-spinner");
|
|
125
|
+
if (spinner) spinner.remove();
|
|
126
|
+
};
|
|
127
|
+
imgEl.addEventListener("load", reveal, { once: true });
|
|
128
|
+
imgEl.addEventListener("error", reveal, { once: true });
|
|
129
|
+
}
|
|
130
|
+
const container = content.slide?.container;
|
|
131
|
+
if (container && !container.querySelector(".pswp-spinner")) {
|
|
132
|
+
container.insertAdjacentHTML("beforeend", SPINNER_SVG);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
userOnBeforeOpen?.(pswp);
|
|
136
|
+
};
|
|
137
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
138
|
+
import_react_photoswipe_gallery.Gallery,
|
|
139
|
+
{
|
|
140
|
+
...rest,
|
|
141
|
+
options: {
|
|
142
|
+
showHideAnimationType: "none",
|
|
143
|
+
showAnimationDuration: 0,
|
|
144
|
+
hideAnimationDuration: 0,
|
|
145
|
+
bgOpacity: 1,
|
|
146
|
+
// User can override any of the above
|
|
147
|
+
...options
|
|
148
|
+
},
|
|
149
|
+
onBeforeOpen: handleBeforeOpen,
|
|
150
|
+
children
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
function AutoSizeItem({
|
|
155
|
+
width,
|
|
156
|
+
height,
|
|
157
|
+
children,
|
|
158
|
+
...rest
|
|
159
|
+
}) {
|
|
160
|
+
const w = width ?? "1";
|
|
161
|
+
const h = height ?? "1";
|
|
162
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_photoswipe_gallery.Item, { ...rest, width: w, height: h, children });
|
|
163
|
+
}
|
|
164
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
165
|
+
0 && (module.exports = {
|
|
166
|
+
AutoSizeGallery,
|
|
167
|
+
AutoSizeItem,
|
|
168
|
+
usePreloadOnHover
|
|
169
|
+
});
|
|
170
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/AutoSizeGallery.tsx"],"sourcesContent":["/**\n * react-photoswipe-autosize\n * ─────────────────────────\n * Drop-in auto-sizing for react-photoswipe-gallery.\n * No width/height props needed — dimensions are detected at runtime.\n *\n * @packageDocumentation\n */\n\nexport { AutoSizeGallery, AutoSizeItem, usePreloadOnHover } from './AutoSizeGallery'\nexport type { AutoSizeGalleryProps, AutoSizeItemProps } from './AutoSizeGallery'\n","/**\n * AutoSizeGallery — PhotoSwipe without image dimensions\n * ──────────────────────────────────────────────────────\n * Drop-in replacements for <Gallery> and <Item> from react-photoswipe-gallery\n * that automatically detect image dimensions at runtime.\n *\n * HOW IT WORKS:\n * 1. Each <AutoSizeItem> renders with a 1×1 placeholder (sentinel value)\n * 2. An `itemData` filter injects cached dimensions before slides are created\n * 3. For uncached images, `contentLoad` fires a background preload\n * 4. Once resolved, `refreshSlideContent` recreates the slide at full size\n *\n * Requires: photoswipe-spinner.css (or react-photoswipe-autosize/styles.css)\n */\n\nimport { useCallback } from 'react'\nimport { Gallery, Item } from 'react-photoswipe-gallery'\nimport type { GalleryProps, ItemProps } from 'react-photoswipe-gallery'\n\n// ─── Spinner SVG markup (injected into slide containers) ────────────\n\nconst SPINNER_SVG = `<svg class=\"pswp-spinner\" viewBox=\"0 0 50 50\">\n <circle class=\"pswp-spinner__path\" cx=\"25\" cy=\"25\" r=\"20\"\n fill=\"none\" stroke-width=\"5\"></circle>\n</svg>`\n\n// ─── Dimension preloader ────────────────────────────────────────────\n\n/** Module-level so it survives re-renders and persists across gallery opens */\nconst dimensionCache = new Map<string, { w: number; h: number }>()\n\nfunction preloadImage(src: string): Promise<{ w: number; h: number }> {\n return new Promise((resolve) => {\n const cached = dimensionCache.get(src)\n if (cached) {\n resolve(cached)\n return\n }\n\n const img = new Image()\n img.onload = () => {\n const dims = { w: img.naturalWidth, h: img.naturalHeight }\n dimensionCache.set(src, dims)\n resolve(dims)\n }\n img.onerror = () => {\n // Fallback: use a reasonable default so PhotoSwipe doesn't break\n const dims = { w: 800, h: 600 }\n resolve(dims)\n }\n img.src = src\n })\n}\n\n// ─── Hover pre-cache hook ───────────────────────────────────────────\n\n/** In-flight preloads — prevents duplicate loads from hover + lightbox open */\nconst inFlightPreloads = new Map<string, Promise<{ w: number; h: number }>>()\n\nfunction startPreload(src: string): Promise<{ w: number; h: number }> {\n const cached = dimensionCache.get(src)\n if (cached) return Promise.resolve(cached)\n\n // Return existing promise to avoid duplicate network requests\n const existing = inFlightPreloads.get(src)\n if (existing) return existing\n\n const promise = preloadImage(src).then((dims) => {\n inFlightPreloads.delete(src)\n return dims\n })\n inFlightPreloads.set(src, promise)\n return promise\n}\n\n/**\n * Returns a handler to pre-cache image dimensions on mouse enter.\n * By the time the user clicks, dimensions are often already known.\n */\nexport function usePreloadOnHover() {\n return useCallback((src: string) => {\n startPreload(src) // fire-and-forget\n }, [])\n}\n\n// ─── AutoSizeGallery ────────────────────────────────────────────────\n\nexport type AutoSizeGalleryProps = Omit<GalleryProps, 'onBeforeOpen'> & {\n /** Your own onBeforeOpen handler (will be called after the auto-size hook) */\n onBeforeOpen?: GalleryProps['onBeforeOpen']\n}\n\nexport function AutoSizeGallery({\n children,\n options,\n onBeforeOpen: userOnBeforeOpen,\n ...rest\n}: AutoSizeGalleryProps) {\n const handleBeforeOpen: GalleryProps['onBeforeOpen'] = (pswp) => {\n // ── Filter: inject cached dimensions BEFORE slide construction ──\n // This runs every time PhotoSwipe reads item data, so if we already\n // know the dimensions from a hover preload, the slide is created\n // at the correct size from the start — no flash, no spinner.\n pswp.addFilter('itemData', (itemData: any) => {\n const src: string | undefined = itemData?.src\n if (!src) return itemData\n\n const cached = dimensionCache.get(src)\n if (cached) {\n // PhotoSwipe reads both .width/.height and .w/.h internally\n itemData.width = cached.w\n itemData.height = cached.h\n itemData.w = cached.w\n itemData.h = cached.h\n }\n return itemData\n })\n\n // ── Disable blurry thumbnail placeholder ──\n // PhotoSwipe normally scales the thumbnail (msrc) up as a blurry preview\n // while the full image loads. We disable this so the user sees our spinner\n // instead of a blur→sharp flash.\n pswp.addFilter('useContentPlaceholder', () => false)\n\n // ── Hook: for uncached images, start preload ──\n pswp.on('contentLoad', (e: any) => {\n const { content } = e\n const src: string | undefined = content?.data?.src\n if (!src) return\n\n // itemData filter already injected dimensions for this slide\n if (dimensionCache.has(src)) return\n\n // Start or join an in-flight preload (deduplicates hover + click)\n startPreload(src).then(() => {\n // Dimensions are now in the cache, so when refreshSlideContent\n // recreates the slide, the itemData filter will inject them.\n try {\n // Look up slide index by URL — content.slide.index may be stale\n // after rapid navigation because PhotoSwipe recycles slide elements.\n const idx = (pswp as any).getNumItems?.()\n ? Array.from({ length: (pswp as any).getNumItems() }, (_, i) => i)\n .find((i: number) => {\n const data = (pswp as any).getItemData?.(i)\n return data?.src === src\n })\n : content.slide?.index\n\n if (idx !== undefined && idx !== -1 && pswp.currSlide) {\n pswp.refreshSlideContent(idx)\n }\n } catch {\n // PhotoSwipe may have closed before preload finished — safe to ignore\n }\n })\n })\n\n // ── Hook: hide image + show spinner while it's loading ──\n pswp.on('contentAppend', (e: any) => {\n const { content } = e\n const src: string | undefined = content?.data?.src\n if (!src) return\n\n const imgEl: HTMLImageElement | undefined = content.element\n\n // > 1 excludes our 1×1 placeholder sentinel\n if (imgEl?.complete && imgEl.naturalWidth > 1) return\n\n // Could be 1×1 placeholder or mid-download — hide until ready\n if (imgEl) {\n imgEl.style.visibility = 'hidden'\n\n\n const reveal = () => {\n imgEl.style.visibility = ''\n const spinner = content.slide?.container?.querySelector('.pswp-spinner')\n if (spinner) spinner.remove()\n }\n imgEl.addEventListener('load', reveal, { once: true })\n imgEl.addEventListener('error', reveal, { once: true })\n }\n\n\n const container: HTMLElement | undefined = content.slide?.container\n if (container && !container.querySelector('.pswp-spinner')) {\n container.insertAdjacentHTML('beforeend', SPINNER_SVG)\n }\n })\n\n\n userOnBeforeOpen?.(pswp)\n }\n\n return (\n <Gallery\n {...rest}\n options={{\n showHideAnimationType: 'none',\n showAnimationDuration: 0,\n hideAnimationDuration: 0,\n bgOpacity: 1,\n // User can override any of the above\n ...options,\n }}\n onBeforeOpen={handleBeforeOpen}\n >\n {children}\n </Gallery>\n )\n}\n\n// ─── AutoSizeItem ───────────────────────────────────────────────────\n\nexport type AutoSizeItemProps = Omit<ItemProps<HTMLElement>, 'width' | 'height'> & {\n /** Override width if you DO know it (bypasses auto-detection for this item) */\n width?: number | string\n /** Override height if you DO know it (bypasses auto-detection for this item) */\n height?: number | string\n}\n\nexport function AutoSizeItem({\n width,\n height,\n children,\n ...rest\n}: AutoSizeItemProps) {\n\n const w = width ?? '1'\n const h = height ?? '1'\n\n return (\n <Item {...rest} width={w} height={h}>\n {children}\n </Item>\n )\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { GalleryProps, ItemProps } from 'react-photoswipe-gallery';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns a handler to pre-cache image dimensions on mouse enter.
|
|
6
|
+
* By the time the user clicks, dimensions are often already known.
|
|
7
|
+
*/
|
|
8
|
+
declare function usePreloadOnHover(): (src: string) => void;
|
|
9
|
+
type AutoSizeGalleryProps = Omit<GalleryProps, 'onBeforeOpen'> & {
|
|
10
|
+
/** Your own onBeforeOpen handler (will be called after the auto-size hook) */
|
|
11
|
+
onBeforeOpen?: GalleryProps['onBeforeOpen'];
|
|
12
|
+
};
|
|
13
|
+
declare function AutoSizeGallery({ children, options, onBeforeOpen: userOnBeforeOpen, ...rest }: AutoSizeGalleryProps): react_jsx_runtime.JSX.Element;
|
|
14
|
+
type AutoSizeItemProps = Omit<ItemProps<HTMLElement>, 'width' | 'height'> & {
|
|
15
|
+
/** Override width if you DO know it (bypasses auto-detection for this item) */
|
|
16
|
+
width?: number | string;
|
|
17
|
+
/** Override height if you DO know it (bypasses auto-detection for this item) */
|
|
18
|
+
height?: number | string;
|
|
19
|
+
};
|
|
20
|
+
declare function AutoSizeItem({ width, height, children, ...rest }: AutoSizeItemProps): react_jsx_runtime.JSX.Element;
|
|
21
|
+
|
|
22
|
+
export { AutoSizeGallery, type AutoSizeGalleryProps, AutoSizeItem, type AutoSizeItemProps, usePreloadOnHover };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { GalleryProps, ItemProps } from 'react-photoswipe-gallery';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns a handler to pre-cache image dimensions on mouse enter.
|
|
6
|
+
* By the time the user clicks, dimensions are often already known.
|
|
7
|
+
*/
|
|
8
|
+
declare function usePreloadOnHover(): (src: string) => void;
|
|
9
|
+
type AutoSizeGalleryProps = Omit<GalleryProps, 'onBeforeOpen'> & {
|
|
10
|
+
/** Your own onBeforeOpen handler (will be called after the auto-size hook) */
|
|
11
|
+
onBeforeOpen?: GalleryProps['onBeforeOpen'];
|
|
12
|
+
};
|
|
13
|
+
declare function AutoSizeGallery({ children, options, onBeforeOpen: userOnBeforeOpen, ...rest }: AutoSizeGalleryProps): react_jsx_runtime.JSX.Element;
|
|
14
|
+
type AutoSizeItemProps = Omit<ItemProps<HTMLElement>, 'width' | 'height'> & {
|
|
15
|
+
/** Override width if you DO know it (bypasses auto-detection for this item) */
|
|
16
|
+
width?: number | string;
|
|
17
|
+
/** Override height if you DO know it (bypasses auto-detection for this item) */
|
|
18
|
+
height?: number | string;
|
|
19
|
+
};
|
|
20
|
+
declare function AutoSizeItem({ width, height, children, ...rest }: AutoSizeItemProps): react_jsx_runtime.JSX.Element;
|
|
21
|
+
|
|
22
|
+
export { AutoSizeGallery, type AutoSizeGalleryProps, AutoSizeItem, type AutoSizeItemProps, usePreloadOnHover };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/AutoSizeGallery.tsx
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
import { Gallery, Item } from "react-photoswipe-gallery";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
var SPINNER_SVG = `<svg class="pswp-spinner" viewBox="0 0 50 50">
|
|
6
|
+
<circle class="pswp-spinner__path" cx="25" cy="25" r="20"
|
|
7
|
+
fill="none" stroke-width="5"></circle>
|
|
8
|
+
</svg>`;
|
|
9
|
+
var dimensionCache = /* @__PURE__ */ new Map();
|
|
10
|
+
function preloadImage(src) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const cached = dimensionCache.get(src);
|
|
13
|
+
if (cached) {
|
|
14
|
+
resolve(cached);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const img = new Image();
|
|
18
|
+
img.onload = () => {
|
|
19
|
+
const dims = { w: img.naturalWidth, h: img.naturalHeight };
|
|
20
|
+
dimensionCache.set(src, dims);
|
|
21
|
+
resolve(dims);
|
|
22
|
+
};
|
|
23
|
+
img.onerror = () => {
|
|
24
|
+
const dims = { w: 800, h: 600 };
|
|
25
|
+
resolve(dims);
|
|
26
|
+
};
|
|
27
|
+
img.src = src;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
var inFlightPreloads = /* @__PURE__ */ new Map();
|
|
31
|
+
function startPreload(src) {
|
|
32
|
+
const cached = dimensionCache.get(src);
|
|
33
|
+
if (cached) return Promise.resolve(cached);
|
|
34
|
+
const existing = inFlightPreloads.get(src);
|
|
35
|
+
if (existing) return existing;
|
|
36
|
+
const promise = preloadImage(src).then((dims) => {
|
|
37
|
+
inFlightPreloads.delete(src);
|
|
38
|
+
return dims;
|
|
39
|
+
});
|
|
40
|
+
inFlightPreloads.set(src, promise);
|
|
41
|
+
return promise;
|
|
42
|
+
}
|
|
43
|
+
function usePreloadOnHover() {
|
|
44
|
+
return useCallback((src) => {
|
|
45
|
+
startPreload(src);
|
|
46
|
+
}, []);
|
|
47
|
+
}
|
|
48
|
+
function AutoSizeGallery({
|
|
49
|
+
children,
|
|
50
|
+
options,
|
|
51
|
+
onBeforeOpen: userOnBeforeOpen,
|
|
52
|
+
...rest
|
|
53
|
+
}) {
|
|
54
|
+
const handleBeforeOpen = (pswp) => {
|
|
55
|
+
pswp.addFilter("itemData", (itemData) => {
|
|
56
|
+
const src = itemData?.src;
|
|
57
|
+
if (!src) return itemData;
|
|
58
|
+
const cached = dimensionCache.get(src);
|
|
59
|
+
if (cached) {
|
|
60
|
+
itemData.width = cached.w;
|
|
61
|
+
itemData.height = cached.h;
|
|
62
|
+
itemData.w = cached.w;
|
|
63
|
+
itemData.h = cached.h;
|
|
64
|
+
}
|
|
65
|
+
return itemData;
|
|
66
|
+
});
|
|
67
|
+
pswp.addFilter("useContentPlaceholder", () => false);
|
|
68
|
+
pswp.on("contentLoad", (e) => {
|
|
69
|
+
const { content } = e;
|
|
70
|
+
const src = content?.data?.src;
|
|
71
|
+
if (!src) return;
|
|
72
|
+
if (dimensionCache.has(src)) return;
|
|
73
|
+
startPreload(src).then(() => {
|
|
74
|
+
try {
|
|
75
|
+
const idx = pswp.getNumItems?.() ? Array.from({ length: pswp.getNumItems() }, (_, i) => i).find((i) => {
|
|
76
|
+
const data = pswp.getItemData?.(i);
|
|
77
|
+
return data?.src === src;
|
|
78
|
+
}) : content.slide?.index;
|
|
79
|
+
if (idx !== void 0 && idx !== -1 && pswp.currSlide) {
|
|
80
|
+
pswp.refreshSlideContent(idx);
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
pswp.on("contentAppend", (e) => {
|
|
87
|
+
const { content } = e;
|
|
88
|
+
const src = content?.data?.src;
|
|
89
|
+
if (!src) return;
|
|
90
|
+
const imgEl = content.element;
|
|
91
|
+
if (imgEl?.complete && imgEl.naturalWidth > 1) return;
|
|
92
|
+
if (imgEl) {
|
|
93
|
+
imgEl.style.visibility = "hidden";
|
|
94
|
+
const reveal = () => {
|
|
95
|
+
imgEl.style.visibility = "";
|
|
96
|
+
const spinner = content.slide?.container?.querySelector(".pswp-spinner");
|
|
97
|
+
if (spinner) spinner.remove();
|
|
98
|
+
};
|
|
99
|
+
imgEl.addEventListener("load", reveal, { once: true });
|
|
100
|
+
imgEl.addEventListener("error", reveal, { once: true });
|
|
101
|
+
}
|
|
102
|
+
const container = content.slide?.container;
|
|
103
|
+
if (container && !container.querySelector(".pswp-spinner")) {
|
|
104
|
+
container.insertAdjacentHTML("beforeend", SPINNER_SVG);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
userOnBeforeOpen?.(pswp);
|
|
108
|
+
};
|
|
109
|
+
return /* @__PURE__ */ jsx(
|
|
110
|
+
Gallery,
|
|
111
|
+
{
|
|
112
|
+
...rest,
|
|
113
|
+
options: {
|
|
114
|
+
showHideAnimationType: "none",
|
|
115
|
+
showAnimationDuration: 0,
|
|
116
|
+
hideAnimationDuration: 0,
|
|
117
|
+
bgOpacity: 1,
|
|
118
|
+
// User can override any of the above
|
|
119
|
+
...options
|
|
120
|
+
},
|
|
121
|
+
onBeforeOpen: handleBeforeOpen,
|
|
122
|
+
children
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
function AutoSizeItem({
|
|
127
|
+
width,
|
|
128
|
+
height,
|
|
129
|
+
children,
|
|
130
|
+
...rest
|
|
131
|
+
}) {
|
|
132
|
+
const w = width ?? "1";
|
|
133
|
+
const h = height ?? "1";
|
|
134
|
+
return /* @__PURE__ */ jsx(Item, { ...rest, width: w, height: h, children });
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
AutoSizeGallery,
|
|
138
|
+
AutoSizeItem,
|
|
139
|
+
usePreloadOnHover
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/AutoSizeGallery.tsx"],"sourcesContent":["/**\n * AutoSizeGallery — PhotoSwipe without image dimensions\n * ──────────────────────────────────────────────────────\n * Drop-in replacements for <Gallery> and <Item> from react-photoswipe-gallery\n * that automatically detect image dimensions at runtime.\n *\n * HOW IT WORKS:\n * 1. Each <AutoSizeItem> renders with a 1×1 placeholder (sentinel value)\n * 2. An `itemData` filter injects cached dimensions before slides are created\n * 3. For uncached images, `contentLoad` fires a background preload\n * 4. Once resolved, `refreshSlideContent` recreates the slide at full size\n *\n * Requires: photoswipe-spinner.css (or react-photoswipe-autosize/styles.css)\n */\n\nimport { useCallback } from 'react'\nimport { Gallery, Item } from 'react-photoswipe-gallery'\nimport type { GalleryProps, ItemProps } from 'react-photoswipe-gallery'\n\n// ─── Spinner SVG markup (injected into slide containers) ────────────\n\nconst SPINNER_SVG = `<svg class=\"pswp-spinner\" viewBox=\"0 0 50 50\">\n <circle class=\"pswp-spinner__path\" cx=\"25\" cy=\"25\" r=\"20\"\n fill=\"none\" stroke-width=\"5\"></circle>\n</svg>`\n\n// ─── Dimension preloader ────────────────────────────────────────────\n\n/** Module-level so it survives re-renders and persists across gallery opens */\nconst dimensionCache = new Map<string, { w: number; h: number }>()\n\nfunction preloadImage(src: string): Promise<{ w: number; h: number }> {\n return new Promise((resolve) => {\n const cached = dimensionCache.get(src)\n if (cached) {\n resolve(cached)\n return\n }\n\n const img = new Image()\n img.onload = () => {\n const dims = { w: img.naturalWidth, h: img.naturalHeight }\n dimensionCache.set(src, dims)\n resolve(dims)\n }\n img.onerror = () => {\n // Fallback: use a reasonable default so PhotoSwipe doesn't break\n const dims = { w: 800, h: 600 }\n resolve(dims)\n }\n img.src = src\n })\n}\n\n// ─── Hover pre-cache hook ───────────────────────────────────────────\n\n/** In-flight preloads — prevents duplicate loads from hover + lightbox open */\nconst inFlightPreloads = new Map<string, Promise<{ w: number; h: number }>>()\n\nfunction startPreload(src: string): Promise<{ w: number; h: number }> {\n const cached = dimensionCache.get(src)\n if (cached) return Promise.resolve(cached)\n\n // Return existing promise to avoid duplicate network requests\n const existing = inFlightPreloads.get(src)\n if (existing) return existing\n\n const promise = preloadImage(src).then((dims) => {\n inFlightPreloads.delete(src)\n return dims\n })\n inFlightPreloads.set(src, promise)\n return promise\n}\n\n/**\n * Returns a handler to pre-cache image dimensions on mouse enter.\n * By the time the user clicks, dimensions are often already known.\n */\nexport function usePreloadOnHover() {\n return useCallback((src: string) => {\n startPreload(src) // fire-and-forget\n }, [])\n}\n\n// ─── AutoSizeGallery ────────────────────────────────────────────────\n\nexport type AutoSizeGalleryProps = Omit<GalleryProps, 'onBeforeOpen'> & {\n /** Your own onBeforeOpen handler (will be called after the auto-size hook) */\n onBeforeOpen?: GalleryProps['onBeforeOpen']\n}\n\nexport function AutoSizeGallery({\n children,\n options,\n onBeforeOpen: userOnBeforeOpen,\n ...rest\n}: AutoSizeGalleryProps) {\n const handleBeforeOpen: GalleryProps['onBeforeOpen'] = (pswp) => {\n // ── Filter: inject cached dimensions BEFORE slide construction ──\n // This runs every time PhotoSwipe reads item data, so if we already\n // know the dimensions from a hover preload, the slide is created\n // at the correct size from the start — no flash, no spinner.\n pswp.addFilter('itemData', (itemData: any) => {\n const src: string | undefined = itemData?.src\n if (!src) return itemData\n\n const cached = dimensionCache.get(src)\n if (cached) {\n // PhotoSwipe reads both .width/.height and .w/.h internally\n itemData.width = cached.w\n itemData.height = cached.h\n itemData.w = cached.w\n itemData.h = cached.h\n }\n return itemData\n })\n\n // ── Disable blurry thumbnail placeholder ──\n // PhotoSwipe normally scales the thumbnail (msrc) up as a blurry preview\n // while the full image loads. We disable this so the user sees our spinner\n // instead of a blur→sharp flash.\n pswp.addFilter('useContentPlaceholder', () => false)\n\n // ── Hook: for uncached images, start preload ──\n pswp.on('contentLoad', (e: any) => {\n const { content } = e\n const src: string | undefined = content?.data?.src\n if (!src) return\n\n // itemData filter already injected dimensions for this slide\n if (dimensionCache.has(src)) return\n\n // Start or join an in-flight preload (deduplicates hover + click)\n startPreload(src).then(() => {\n // Dimensions are now in the cache, so when refreshSlideContent\n // recreates the slide, the itemData filter will inject them.\n try {\n // Look up slide index by URL — content.slide.index may be stale\n // after rapid navigation because PhotoSwipe recycles slide elements.\n const idx = (pswp as any).getNumItems?.()\n ? Array.from({ length: (pswp as any).getNumItems() }, (_, i) => i)\n .find((i: number) => {\n const data = (pswp as any).getItemData?.(i)\n return data?.src === src\n })\n : content.slide?.index\n\n if (idx !== undefined && idx !== -1 && pswp.currSlide) {\n pswp.refreshSlideContent(idx)\n }\n } catch {\n // PhotoSwipe may have closed before preload finished — safe to ignore\n }\n })\n })\n\n // ── Hook: hide image + show spinner while it's loading ──\n pswp.on('contentAppend', (e: any) => {\n const { content } = e\n const src: string | undefined = content?.data?.src\n if (!src) return\n\n const imgEl: HTMLImageElement | undefined = content.element\n\n // > 1 excludes our 1×1 placeholder sentinel\n if (imgEl?.complete && imgEl.naturalWidth > 1) return\n\n // Could be 1×1 placeholder or mid-download — hide until ready\n if (imgEl) {\n imgEl.style.visibility = 'hidden'\n\n\n const reveal = () => {\n imgEl.style.visibility = ''\n const spinner = content.slide?.container?.querySelector('.pswp-spinner')\n if (spinner) spinner.remove()\n }\n imgEl.addEventListener('load', reveal, { once: true })\n imgEl.addEventListener('error', reveal, { once: true })\n }\n\n\n const container: HTMLElement | undefined = content.slide?.container\n if (container && !container.querySelector('.pswp-spinner')) {\n container.insertAdjacentHTML('beforeend', SPINNER_SVG)\n }\n })\n\n\n userOnBeforeOpen?.(pswp)\n }\n\n return (\n <Gallery\n {...rest}\n options={{\n showHideAnimationType: 'none',\n showAnimationDuration: 0,\n hideAnimationDuration: 0,\n bgOpacity: 1,\n // User can override any of the above\n ...options,\n }}\n onBeforeOpen={handleBeforeOpen}\n >\n {children}\n </Gallery>\n )\n}\n\n// ─── AutoSizeItem ───────────────────────────────────────────────────\n\nexport type AutoSizeItemProps = Omit<ItemProps<HTMLElement>, 'width' | 'height'> & {\n /** Override width if you DO know it (bypasses auto-detection for this item) */\n width?: number | string\n /** Override height if you DO know it (bypasses auto-detection for this item) */\n height?: number | string\n}\n\nexport function AutoSizeItem({\n width,\n height,\n children,\n ...rest\n}: AutoSizeItemProps) {\n\n const w = width ?? '1'\n const h = height ?? '1'\n\n return (\n <Item {...rest} width={w} height={h}>\n {children}\n </Item>\n )\n}\n"],"mappings":"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","names":[]}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* react-photoswipe-autosize — Spinner + Theme
|
|
3
|
+
* ────────────────────────────────────────────
|
|
4
|
+
* Loading spinner shown while image dimensions are detected.
|
|
5
|
+
* Includes an optional light theme (white bg, black UI).
|
|
6
|
+
*
|
|
7
|
+
* Import this file in your app:
|
|
8
|
+
* import 'react-photoswipe-autosize/styles.css'
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/* ─── Spinner ─── */
|
|
12
|
+
|
|
13
|
+
.pswp-spinner {
|
|
14
|
+
animation: pswp-rotate 2s linear infinite;
|
|
15
|
+
z-index: 2;
|
|
16
|
+
position: absolute;
|
|
17
|
+
top: 50%;
|
|
18
|
+
left: 50%;
|
|
19
|
+
margin: -25px 0 0 -25px;
|
|
20
|
+
width: 50px;
|
|
21
|
+
height: 50px;
|
|
22
|
+
pointer-events: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.pswp-spinner .pswp-spinner__path {
|
|
26
|
+
stroke: var(--pswp-spinner-color, var(--pswp-icon-color, #fff));
|
|
27
|
+
stroke-linecap: round;
|
|
28
|
+
animation: pswp-dash 1.5s ease-in-out infinite;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@keyframes pswp-rotate {
|
|
32
|
+
100% {
|
|
33
|
+
transform: rotate(360deg);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@keyframes pswp-dash {
|
|
38
|
+
0% {
|
|
39
|
+
stroke-dasharray: 1, 150;
|
|
40
|
+
stroke-dashoffset: 0;
|
|
41
|
+
}
|
|
42
|
+
50% {
|
|
43
|
+
stroke-dasharray: 90, 150;
|
|
44
|
+
stroke-dashoffset: -35;
|
|
45
|
+
}
|
|
46
|
+
100% {
|
|
47
|
+
stroke-dasharray: 90, 150;
|
|
48
|
+
stroke-dashoffset: -124;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-photoswipe-autosize",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Drop-in auto-sizing for react-photoswipe-gallery — no width/height props needed",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/index.d.cts",
|
|
18
|
+
"default": "./dist/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"./styles.css": "./dist/styles.css"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"postbuild": "node -e \"require('fs').copyFileSync('src/styles.css','dist/styles.css')\"",
|
|
31
|
+
"dev": "tsup --watch",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"photoswipe": "^5.0.0",
|
|
36
|
+
"react": ">=17.0.0",
|
|
37
|
+
"react-photoswipe-gallery": ">=2.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/react": "^19.0.0",
|
|
41
|
+
"photoswipe": "^5.4.4",
|
|
42
|
+
"react": "^19.0.0",
|
|
43
|
+
"react-photoswipe-gallery": "^3.0.2",
|
|
44
|
+
"tsup": "^8.0.0",
|
|
45
|
+
"typescript": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"photoswipe",
|
|
49
|
+
"react",
|
|
50
|
+
"gallery",
|
|
51
|
+
"lightbox",
|
|
52
|
+
"auto-size",
|
|
53
|
+
"dimensions",
|
|
54
|
+
"react-photoswipe-gallery"
|
|
55
|
+
],
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "https://github.com/imontaine/react-photoswipe-autosize.git"
|
|
59
|
+
}
|
|
60
|
+
}
|