popupable 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/README.md +203 -0
- package/dist/popupable.min.css +7 -0
- package/dist/popupable.min.js +7 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# popupable
|
|
2
|
+
|
|
3
|
+
A lightweight, zero-dependency lightbox library using modern JavaScript and CSS.
|
|
4
|
+
Just add `data-popupable` to any image!
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/popupable)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
[**Live Demo**](https://popupable.ewanhowell.com/)
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
* No dependencies
|
|
14
|
+
* Animates open from the element's original position
|
|
15
|
+
* Works with mouse, touch, and keyboard
|
|
16
|
+
* Gallery groups with swipe, scroll, keyboard, and button navigation
|
|
17
|
+
* Thumbnail strip and image counter
|
|
18
|
+
* Pinch-to-zoom on touch, with optional click/tap-to-zoom support
|
|
19
|
+
* Load a hi-res image on open
|
|
20
|
+
* Checkerboard background for transparent images
|
|
21
|
+
* Customizable via CSS variables
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### Install via npm
|
|
26
|
+
```bash
|
|
27
|
+
npm install popupable
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Or use via CDN
|
|
31
|
+
https://www.jsdelivr.com/package/npm/popupable
|
|
32
|
+
|
|
33
|
+
### Import
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
import "popupable/styles.css"
|
|
37
|
+
import "popupable"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Add popups to your images
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<img src="photo.jpg" data-popupable>
|
|
44
|
+
<img src="thumbnail.jpg" data-popupable data-popupable-src="full-res.jpg">
|
|
45
|
+
<img src="photo.jpg" data-popupable data-popupable-title="Sunset" data-popupable-description="Taken in July 2024">
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Close by clicking again, or pressing Escape, Backspace, or Delete.
|
|
49
|
+
|
|
50
|
+
## Settings
|
|
51
|
+
|
|
52
|
+
| Attribute | Description |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `data-popupable` | Required. Marks an image as openable. Optionally set a value to give the popup a CSS `id`. |
|
|
55
|
+
| `data-popupable-src="url"` | A different image to display when the popup is open, e.g. a higher resolution version. |
|
|
56
|
+
| `data-popupable-title="text"` | Title text shown alongside the image. |
|
|
57
|
+
| `data-popupable-description="text"` | Description text shown alongside the image. |
|
|
58
|
+
| `data-popupable-transparent` | Shows a checkerboard background behind transparent images. |
|
|
59
|
+
| `data-popupable-maintain-aspect` | Uses the element's rendered aspect ratio instead of the image's natural dimensions. |
|
|
60
|
+
| `data-popupable-no-upscale` | Prevents the popup from scaling the image beyond its native resolution. |
|
|
61
|
+
| `data-popupable-zoomable` | Enables click/tap-to-zoom and scroll wheel zoom. |
|
|
62
|
+
| `data-popupable-group="name"` | Groups images into a navigable gallery. |
|
|
63
|
+
| `data-popupable-counter` | Shows a "1 / N" counter when in a group. Read from the clicked image. |
|
|
64
|
+
| `data-popupable-thumbnails` | Shows a thumbnail strip when in a group. Read from the clicked image. |
|
|
65
|
+
| `data-popupable-order="..."` | Controls the order of UI elements. See [Custom UI order](#custom-ui-order). |
|
|
66
|
+
|
|
67
|
+
## Advanced Usage
|
|
68
|
+
|
|
69
|
+
### Hi-res source
|
|
70
|
+
|
|
71
|
+
Use `data-popupable-src` to display a different (e.g. higher resolution) image when the popup is open:
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<img src="thumbnail.jpg" data-popupable data-popupable-src="full-res.jpg">
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Transparent images
|
|
78
|
+
|
|
79
|
+
Add `data-popupable-transparent` to show a checkerboard background behind transparent images:
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<img src="icon.png" data-popupable data-popupable-transparent>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Maintain aspect ratio
|
|
86
|
+
|
|
87
|
+
By default, the popup size is calculated from the image's natural dimensions. Add `data-popupable-maintain-aspect` to use the element's rendered aspect ratio instead, which is useful when the image is cropped or stretched via CSS:
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<img src="photo.jpg" data-popupable data-popupable-maintain-aspect>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Prevent upscaling
|
|
94
|
+
|
|
95
|
+
Add `data-popupable-no-upscale` to prevent the popup from scaling the image beyond its native resolution:
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<img src="pixel-art.png" data-popupable data-popupable-no-upscale>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Zoom
|
|
102
|
+
|
|
103
|
+
All images support pinch-to-zoom. Add `data-popupable-zoomable` to also enable tap/click-to-zoom and scroll wheel zoom:
|
|
104
|
+
|
|
105
|
+
```html
|
|
106
|
+
<img src="map.jpg" data-popupable data-popupable-zoomable>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
When zoomed in, pan by dragging and zoom in/out with the scroll wheel. Click the image or background, or pinch back to scale 1, to unzoom.
|
|
110
|
+
|
|
111
|
+
### Groups / Galleries
|
|
112
|
+
|
|
113
|
+
Group multiple images together with `data-popupable-group`. Users can navigate with arrow buttons, swipe gestures, scroll wheel, or keyboard shortcuts.
|
|
114
|
+
|
|
115
|
+
```html
|
|
116
|
+
<img src="photo1.jpg" data-popupable data-popupable-group="holiday">
|
|
117
|
+
<img src="photo2.jpg" data-popupable data-popupable-group="holiday">
|
|
118
|
+
<img src="photo3.jpg" data-popupable data-popupable-group="holiday">
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Counter and thumbnails
|
|
122
|
+
|
|
123
|
+
Add `data-popupable-counter` and/or `data-popupable-thumbnails` to individual group members. The attributes are read from whichever image is clicked to open the gallery, so add them to every image that should show these elements:
|
|
124
|
+
|
|
125
|
+
```html
|
|
126
|
+
<img src="photo1.jpg" data-popupable data-popupable-group="holiday" data-popupable-counter data-popupable-thumbnails>
|
|
127
|
+
<img src="photo2.jpg" data-popupable data-popupable-group="holiday" data-popupable-counter data-popupable-thumbnails>
|
|
128
|
+
<img src="photo3.jpg" data-popupable data-popupable-group="holiday" data-popupable-counter data-popupable-thumbnails>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### Custom UI order
|
|
132
|
+
|
|
133
|
+
Control the order of UI elements around the image with `data-popupable-order`. The `image` token marks where the image sits; everything before it goes in the header, everything after goes in the footer.
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<!-- Counter above image, thumbnails and content below -->
|
|
137
|
+
<img data-popupable data-popupable-order="counter,image,thumbnails,content">
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Default order: `counter,image,content,thumbnails`
|
|
141
|
+
|
|
142
|
+
#### Keyboard navigation
|
|
143
|
+
|
|
144
|
+
| Keys | Action |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `→` `↓` `Page Down` `D` `S` | Next image |
|
|
147
|
+
| `←` `↑` `Page Up` `A` `W` | Previous image |
|
|
148
|
+
| `Home` | First image |
|
|
149
|
+
| `End` | Last image |
|
|
150
|
+
| `1`–`9` | Jump to image by number |
|
|
151
|
+
|
|
152
|
+
### Custom IDs
|
|
153
|
+
|
|
154
|
+
Set the value of `data-popupable` to give the popup a CSS `id`, useful for per-popup styling. In a gallery, the `id` updates as you navigate, so each image can have its own style:
|
|
155
|
+
|
|
156
|
+
```html
|
|
157
|
+
<img src="red.jpg" data-popupable="theme-red" data-popupable-group="themed">
|
|
158
|
+
<img src="green.jpg" data-popupable="theme-green" data-popupable-group="themed">
|
|
159
|
+
<img src="blue.jpg" data-popupable="theme-blue" data-popupable-group="themed">
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```css
|
|
163
|
+
#theme-red { --popupable-background: #3a0000cc; }
|
|
164
|
+
#theme-green { --popupable-background: #003a00cc; }
|
|
165
|
+
#theme-blue { --popupable-background: #00003acc; }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Customization
|
|
169
|
+
|
|
170
|
+
Popups can be styled using CSS variables:
|
|
171
|
+
|
|
172
|
+
```css
|
|
173
|
+
:root {
|
|
174
|
+
/* Overlay */
|
|
175
|
+
--popupable-background: #000B; /* Backdrop color */
|
|
176
|
+
--popupable-blur: 6px; /* Backdrop blur amount */
|
|
177
|
+
|
|
178
|
+
/* UI elements (header, footer, buttons) */
|
|
179
|
+
--popupable-ui-background: #0008; /* Header/footer/button background */
|
|
180
|
+
|
|
181
|
+
/* Spacing */
|
|
182
|
+
--popupable-screen-padding: 40px; /* Minimum gap between image and viewport edge */
|
|
183
|
+
|
|
184
|
+
/* Animation */
|
|
185
|
+
--popupable-open-duration: .25s; /* Open/close transition duration */
|
|
186
|
+
--popupable-switch-duration: .25s; /* Gallery navigation transition duration */
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## How it works
|
|
191
|
+
|
|
192
|
+
popupable uses a clone-and-expand technique:
|
|
193
|
+
|
|
194
|
+
1. **Clones the image** at its exact on-screen position and size
|
|
195
|
+
2. **Hides the original** and appends the clone as a fixed overlay
|
|
196
|
+
3. **Animates the clone** to fill the viewport, respecting aspect ratio and padding
|
|
197
|
+
4. **On close**, reverses the animation back to the original position, then removes the overlay
|
|
198
|
+
|
|
199
|
+
This gives a seamless visual transition with no layout shifts.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT © [Ewan Howell](https://github.com/ewanhowell5195)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* popupable
|
|
3
|
+
* Version : 1.0.0
|
|
4
|
+
* License : MIT
|
|
5
|
+
* Copyright: 2025 Ewan Howell
|
|
6
|
+
*/
|
|
7
|
+
@property --popupable-screen-padding{syntax:"<length>";inherits:true;initial-value:40px}@property --popupable-background{syntax:"<color>";inherits:true;initial-value:#000B}@property --popupable-ui-background{syntax:"<color>";inherits:true;initial-value:#0008}@property --popupable-blur{syntax:"<length>";inherits:true;initial-value:6px}@property --popupable-open-duration{syntax:"<time>";inherits:true;initial-value:.25s}@property --popupable-switch-duration{syntax:"<time>";inherits:true;initial-value:.25s}[data-popupable],[data-popupable] *{cursor:pointer!important}.popupable-hide{visibility:hidden!important}.popupable-container{position:fixed;inset:0;transition:background var(--popupable-open-duration),backdrop-filter var(--popupable-open-duration);z-index:2147483646;user-select:none;pointer-events:none}.popupable-container.popupable-active{z-index:2147483647;background:var(--popupable-background);backdrop-filter:blur(var(--popupable-blur))}.popupable-container>*{pointer-events:none}.popupable-viewport{position:fixed;left:var(--popupable-vv-left,0);top:var(--popupable-vv-top,0);width:calc(var(--popupable-vv-width,100vw) * var(--popupable-vv-scale,1));height:calc(var(--popupable-vv-height,100vh) * var(--popupable-vv-scale,1));transform-origin:top left;transform:scale(var(--popupable-vv-ui-scale,1));pointer-events:none!important}.popupable-container *{box-sizing:border-box}.popupable-container.popupable-open,.popupable-container.popupable-open>*{pointer-events:initial}.popupable-clones{transition:transform var(--popupable-switch-duration)}.popupable-clone-container{position:fixed;transition:all var(--popupable-open-duration);pointer-events:initial;overflow:hidden;transform:translateX(calc(var(--popupable-view-width) * var(--popupable-offset-multiplier)));transform-origin:0 0}.popupable-clone-container.popupable-transparent::before{content:"";position:absolute;inset:0;background-image:conic-gradient(#313131 .25turn,#1e1e1e .25turn .5turn,#313131 .5turn .75turn,#1e1e1e .75turn);background-size:32px 32px;background-attachment:fixed;z-index:-1;opacity:0;transition:opacity var(--popupable-open-duration);image-rendering:pixelated}.popupable-container.popupable-active .popupable-clone-container{border-radius:0!important}.popupable-container.popupable-active .popupable-clone-container.popupable-transparent::before{opacity:1}.popupable-container.popupable-open .popupable-clone-container{transition:transform var(--popupable-switch-duration),translate var(--popupable-switch-duration)}.popupable-clone,.popupable-clone-layer{width:100%;height:100%;position:absolute;inset:0;transition:opacity var(--popupable-open-duration)}.popupable-clone-layer{opacity:0;object-fit:cover}.popupable-container.popupable-open .popupable-clone:not(:last-child){opacity:0}.popupable-container.popupable-active .popupable-clone-layer{opacity:1}.popupable-button{width:40px;height:40px;display:flex;align-items:center;justify-content:center;transition:opacity var(--popupable-open-duration),transform var(--popupable-open-duration);cursor:pointer;position:relative}.popupable-button::before{content:"";position:absolute;inset:0;opacity:.5;background:var(--popupable-ui-background);transition:opacity .25s;border-radius:50%}.popupable-button svg{width:24px;height:24px;z-index:1}.popupable-container.popupable-open .popupable-button{transition:background .25s,opacity var(--popupable-open-duration),transform .25s}.popupable-button:hover::before,.popupable-next-container:hover .popupable-button::before,.popupable-prev-container:hover .popupable-button::before{opacity:1}.popupable-next-container,.popupable-prev-container{position:absolute;top:50%;transform:translateY(-50%);padding:40px;cursor:pointer;z-index:1;pointer-events:auto}.popupable-next-container{right:calc(var(--popupable-screen-padding)/ 2)}.popupable-prev-container{left:calc(var(--popupable-screen-padding)/ 2)}.popupable-nav-button{opacity:0}.popupable-next{transform:translateX(40px)}.popupable-prev{transform:translateX(-40px)}.popupable-container.popupable-active .popupable-nav-button{opacity:1;transform:translateX(0)}.popupable-next-container:hover .popupable-button{transform:translateX(4px)}.popupable-prev-container:hover .popupable-button{transform:translateX(-4px)}.popupable-locked .popupable-next-container,.popupable-locked .popupable-prev-container,.popupable-next-container.popupable-disabled,.popupable-next-container.popupable-nav-inactive,.popupable-prev-container.popupable-disabled,.popupable-prev-container.popupable-nav-inactive{pointer-events:none}.popupable-locked .popupable-next-container .popupable-next,.popupable-next-container.popupable-disabled .popupable-next,.popupable-next-container.popupable-nav-inactive .popupable-next{transform:translateX(40px);opacity:0}.popupable-locked .popupable-prev-container .popupable-prev,.popupable-prev-container.popupable-disabled .popupable-prev,.popupable-prev-container.popupable-nav-inactive .popupable-prev{transform:translateX(-40px);opacity:0}.popupable-container:not(.popupable-active) .popupable-clone-container:not(.popupable-clone-extra){transform:initial}.popupable-container:not(.popupable-active) .popupable-clone-extra{opacity:0;scale:0.75}.popupable-footer,.popupable-header{position:absolute;top:0;left:0;right:0;opacity:0;transition:opacity var(--popupable-open-duration),transform var(--popupable-open-duration);transform:translateY(-40px);color:#fff;background:var(--popupable-ui-background);pointer-events:auto}.popupable-footer{top:initial;bottom:0;transform:translateY(40px)}.popupable-container.popupable-active:not(.popupable-locked) .popupable-footer,.popupable-container.popupable-active:not(.popupable-locked) .popupable-header{opacity:1;transform:initial}.popupable-counter{padding:10px;text-align:center}.popupable-footer>*+*,.popupable-header>*+*{border-top:1px solid #fff3}.popupable-content-container{display:grid;align-items:flex-end;transition:height var(--popupable-switch-duration);overflow:hidden;position:relative}.popupable-thumbnails{display:flex;gap:10px;padding:10px;justify-content:safe center;overflow:hidden;scrollbar-width:none;touch-action:pan-x}.popupable-thumbnails.popupable-thumbnails-dragging{cursor:grabbing}.popupable-thumbnail{width:64px;height:64px;object-fit:cover;opacity:.65;cursor:pointer;transition:opacity .2s,outline-color .2s;outline:2px solid #0000;outline-offset:-2px;flex:0 0 auto}.popupable-thumbnail:hover{opacity:1}.popupable-thumbnail.popupable-thumbnail-active{opacity:1;outline-color:#fffa}.popupable-content{grid-column:1;grid-row:1;text-align:center;padding:16px;color:#fff;display:flex;flex-direction:column;gap:8px;user-select:text;transition:opacity var(--popupable-switch-duration),transform var(--popupable-switch-duration);position:absolute;bottom:0;left:50%;transform:translateX(-50%);max-width:100%;width:max-content}.popupable-title{font-size:32px;font-weight:600}.popupable-content-before{pointer-events:none;opacity:0;transform:translateX(calc(-50% - 80px))}.popupable-content-after{pointer-events:none;opacity:0;transform:translateX(calc(-50% + 80px))}.popupable-zoomable{cursor:zoom-in}.popupable-zoomed{cursor:zoom-out;touch-action:none}@media (max-width:768px){:root{--popupable-screen-padding:14px}.popupable-button{width:42px;height:42px}.popupable-next-container,.popupable-prev-container{padding:36px}.popupable-button svg{width:25px;height:25px}.popupable-counter{padding:8px}.popupable-content{padding:14px;gap:6px}.popupable-title{font-size:28px}.popupable-thumbnail{width:48px;height:48px}}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* popupable
|
|
3
|
+
* Version : 1.0.0
|
|
4
|
+
* License : MIT
|
|
5
|
+
* Copyright: 2025 Ewan Howell
|
|
6
|
+
*/
|
|
7
|
+
{let e,t,n,o=0;function disableScroll(){window.addEventListener("wheel",prevent,{passive:!1}),window.addEventListener("touchmove",prevent,{passive:!1}),window.addEventListener("keydown",blockKeys,!0)}function enableScroll(){window.removeEventListener("wheel",prevent),window.removeEventListener("touchmove",prevent),window.removeEventListener("keydown",blockKeys,!0)}function prevent(e){e.preventDefault()}function blockKeys(e){["ArrowUp","ArrowDown","PageUp","PageDown","Home","End"," "].includes(e.key)&&e.preventDefault()}function setCloneToOriginalRect(e,t){const n=t.getBoundingClientRect();e.style.top=visualViewport.offsetTop+n.top+"px",e.style.left=visualViewport.offsetLeft+n.left+"px",e.style.width=n.width+"px",e.style.height=n.height+"px"}function openPopupable(e){if("open"===e.state)return;e.state="open";const{cloneContainer:t,popup:n,transition:o,group:a,listeners:r}=e;n.classList.add("popupable-active"),updateExpandedSize(),t.removeEventListener("transitionend",o.listener),o.listener=e=>{if(!e||e.target===e.currentTarget){if(t.removeEventListener("transitionend",o.listener),n.classList.add("popupable-open"),a)for(const e of a)e.cloneContainer.style.display=null;for(const e of r)e.target.addEventListener(e.event,e.func,e.args)}},o.duration?t.addEventListener("transitionend",o.listener):o.listener()}function closePopupable(){if(!e||"close"===e.state)return;o++,e.state="close";const{cloneContainer:n,clone:a,original:r,popup:i,transition:l,group:s,listeners:p}=e;if(i.classList.remove("popupable-active"),i.classList.remove("popupable-open"),setCloneToOriginalRect(n,r),s)for(const[e,t]of s.entries())t.clone!==a&&e!==s.currentIndex&&(t.cloneContainer.style.display="none");for(const e of p)e.target.removeEventListener(e.event,e.func);n.removeEventListener("transitionend",l.listener);const c=e;l.listener=o=>{o&&o.target!==o.currentTarget||(n.removeEventListener("transitionend",l.listener),r.classList.remove("popupable-hide"),i.remove(),c===e&&(enableScroll(),t=e,e=null))},l.duration?n.addEventListener("transitionend",l.listener):l.listener()}function updateExpandedSize(){if(!e||"close"===e.state)return;const t=visualViewport?.width||window.innerWidth,n=visualViewport?.height||window.innerHeight,o=visualViewport?.offsetTop||0,a=visualViewport?.offsetLeft||0,r=visualViewport?.scale||1;document.documentElement.style.setProperty("--popupable-view-width",t+"px"),e.popup.style.setProperty("--popupable-vv-width",t+"px"),e.popup.style.setProperty("--popupable-vv-height",n+"px"),e.popup.style.setProperty("--popupable-vv-top",o+"px"),e.popup.style.setProperty("--popupable-vv-left",a+"px"),e.popup.style.setProperty("--popupable-vv-scale",r),e.popup.style.setProperty("--popupable-vv-ui-scale",1/r);const i=parseFloat(getComputedStyle(e.popup).getPropertyValue("--popupable-vv-ui-scale"))||1,l=(parseFloat(getComputedStyle(e.popup).getPropertyValue("--popupable-screen-padding"))||0)/r,s=Math.max(0,t-2*l),p=n-2*l,c=e.popup.querySelector(".popupable-counter"),u=c?c.getBoundingClientRect().height/i:0;let d;d=e.group?e.group:[e];for(const t of d){let c;if(t.maintainAspect){const e=t.original.getBoundingClientRect();c=e.width/e.height}else c=t.cloneLayer?t.cloneLayer.naturalWidth/t.cloneLayer.naturalHeight:t.original.naturalWidth/t.original.naturalHeight;const d=Math.max(0,p),f=t.content?t.content.getBoundingClientRect().height/i:0,m=e.thumbnailsContainer?e.thumbnailsContainer.getBoundingClientRect().height/i:0,b=(e.orderPlacement.counterTop?u:0)+(e.orderPlacement.contentTop?f:0)+(e.orderPlacement.thumbnailsTop?m:0),v=(e.orderPlacement.counterBottom?u:0)+(e.orderPlacement.contentBottom?f:0)+(e.orderPlacement.thumbnailsBottom?m:0),h=Math.max(0,n-b-v-2*l);let g=s,y=g/c;if(y>d&&(y=d,g=y*c),y>h&&(y=h,g=y*c),t.noUpscale){const e=t.cloneLayer||t.original,n=e.naturalWidth,o=e.naturalHeight;if(n&&o){const e=n/r,t=o/r,a=Math.min(1,e/g,t/y);a<1&&(g*=a,y*=a)}}let L=o+l+(d-y)/2;const x=o+n-v-l-y;L=Math.min(L,x),L=Math.max(L,o+b+l),t.cloneContainer.style.top=L+"px",t.cloneContainer.style.left=a+l+(s-g)/2+"px",t.cloneContainer.style.width=g+"px",t.cloneContainer.style.height=y+"px"}if(e.contentContainer){let t;t=e.group?e.group[e.group.currentIndex]:e;const n=t.content.getBoundingClientRect();e.contentContainer.style.height=n.height/i+"px"}}function parsePopupableOrder(e){const t=["counter","image","content","thumbnails"],n=new Set(t),o=[];if(e)for(const t of e.split(",")){const e=t.trim().toLowerCase();e&&n.has(e)&&!o.includes(e)&&o.push(e)}for(const e of t)o.includes(e)||o.push(e);const a=o.indexOf("image");return{top:o.slice(0,a),bottom:o.slice(a+1)}}function cloneElement(e){const t=document.createElement("div");t.className="popupable-clone-container",e.hasAttribute("data-popupable-transparent")&&t.classList.add("popupable-transparent");const n=new Image;n.className="popupable-clone",n.src=e.currentSrc??e.src;const o=getComputedStyle(e);let a,r,i;if(t.style.borderRadius=o.borderRadius,n.style.objectFit=o.objectFit,n.style.objectPosition=o.objectPosition,n.style.imageRendering=o.imageRendering,n.style.background=o.background,t.append(n),e.dataset.popupableSrc&&(a=new Image,a.className="popupable-clone-layer",a.src=e.dataset.popupableSrc,a.style.imageRendering=o.imageRendering,t.append(a),"fill"===n.style.objectFit)){const t=e.getBoundingClientRect();e.naturalWidth&&e.naturalHeight&&Math.abs(t.width/t.height-e.naturalWidth/e.naturalHeight)<.01&&(n.style.objectFit="cover")}if(e.dataset.popupableTitle||e.dataset.popupableDescription){if(r=document.createElement("div"),r.classList="popupable-content",e.dataset.popupableTitle){const t=document.createElement("div");t.className="popupable-title",t.textContent=e.dataset.popupableTitle,r.append(t)}if(e.dataset.popupableDescription){const t=document.createElement("div");t.className="popupable-description",t.textContent=e.dataset.popupableDescription,r.append(t)}}return e.hasAttribute("data-popupable-zoomable")&&(i=!0,t.classList.add("popupable-zoomable")),{id:e.dataset.popupable,original:e,cloneContainer:t,clone:n,cloneLayer:a,maintainAspect:e.hasAttribute("data-popupable-maintain-aspect"),noUpscale:e.hasAttribute("data-popupable-no-upscale"),counter:e.hasAttribute("data-popupable-counter"),thumbnails:e.hasAttribute("data-popupable-thumbnails"),order:parsePopupableOrder(e.dataset.popupableOrder),ready:Promise.all([n,a].filter(Boolean).map(e=>e.complete?Promise.resolve():new Promise(t=>{e.addEventListener("load",t,{once:!0}),e.addEventListener("error",t,{once:!0})}))),content:r,zoomable:i}}let a,r,i,l=0;function handleMove(t){if("open"!==e?.state||!e.group||!a)return;const n=e.group[e.group.currentIndex];n.cloneContainer.parentElement.style.transition="initial",n.cloneContainer.parentElement.style.transform=`translateX(${(t.touches?.[0].clientX??t.clientX)-r}px)`}document.addEventListener("pointerdown",t=>{0===t.button&&("touch"===t.pointerType&&l++,a||(n=t.target),a||"open"!==e?.state||t.target.closest(".popupable-header, .popupable-footer")||(a=!0,r=t.clientX,i=t.clientY))}),document.addEventListener("mousemove",handleMove),document.addEventListener("touchmove",handleMove,{passive:!0}),document.addEventListener("pointerup",async s=>{if(0!==s.button)return;if("touch"===s.pointerType&&(l=Math.max(0,l-1),a&&l>=1))return;if(a){a=!1;const U=e.group?e.group[e.group.currentIndex]:e;U.cloneContainer.parentElement.style.transition=null,U.cloneContainer.parentElement.style.transform=null;const _=s.clientX-r,j=Math.abs(_),K=s.clientY-i,G=Math.abs(K);if("touch"===s.pointerType&&G>56&&G>1.1*j)return void closePopupable();if(e.group&&j>3){const Z=Math.max(0,Math.floor((j-window.innerWidth/2)/window.innerWidth));if(_>32)for(let J=0;J<=Z;J++)e.goPrev();else if(_<-32)for(let Q=0;Q<=Z;Q++)e.goNext();return void(e.blocked=!0)}}const p=s.target.closest(".popupable-viewport")&&!n.closest(".popupable-viewport");if(!(p||s.target==n||n.classList.contains("popupable-clone-container")&&s.target===t?.original))return;const c=(p?n.closest("[data-popupable]"):null)||s.target.closest("[data-popupable]");if(!c){if(p)return void closePopupable();if(s.target.closest(".popupable-container"))return;return void(e&&("zoomed"===e.state?e.unzoom():closePopupable()))}if(s.preventDefault(),e?.original===c&&e.popup&&!e.popup.isConnected&&"close"!==e.state)return;const u=++o;e&&closePopupable(),e={transition:{},listeners:[]};const d=e,f=document.createElement("div");f.className="popupable-clones";const m=cloneElement(c),{cloneContainer:b,clone:v,content:h}=m;let g;if(c.dataset.popupableGroup){const ee=document.querySelectorAll(`[data-popupable-group="${c.dataset.popupableGroup}"]`);if(ee.length){g=[];for(const[te,ne]of ee.entries())if(ne===c)g.push(m),g.currentIndex=te,f.append(b);else{const oe=cloneElement(ne);oe.cloneContainer.style.display="none",oe.cloneContainer.classList.add("popupable-clone-extra"),g.push(oe),f.append(oe.cloneContainer)}}}else f.append(b);const y=document.createElement("div");y.className="popupable-container",m.id&&(y.id=m.id);const L=document.createElement("div");let x,w,C,P,E,I,M,z,k,T;L.className="popupable-viewport",h&&(x=document.createElement("div"),x.classList="popupable-content-container");const X={};if(g){m.counter&&(w=document.createElement("div"),w.className="popupable-header",P=document.createElement("div"),P.className="popupable-counter",w.append(P)),m.thumbnails&&(E=document.createElement("div"),E.className="popupable-thumbnails",I=g.map((e,t)=>{const n=new Image;return n.className="popupable-thumbnail",n.src=e.original.currentSrc??e.original.src,n.dataset.thumbnailIndex=t,E.append(n),n})),L.innerHTML=`\n <div class="popupable-prev-container${g.currentIndex?"":" popupable-disabled"}">\n <div class="popupable-button popupable-nav-button popupable-prev">\n <svg width="24px" height="24px" viewBox="0 -960 960 960" fill="#fff">\n <path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"/>\n </svg>\n </div>\n </div>\n <div class="popupable-next-container${g.currentIndex===g.length-1?" popupable-disabled":""}">\n <div class="popupable-button popupable-nav-button popupable-next">\n <svg width="24px" height="24px" viewBox="0 -960 960 960" fill="#fff">\n <path d="M647-440H160v-80h487L423-744l57-56 320 320-320 320-57-56 224-224Z"/>\n </svg>\n </div>\n </div>\n `;const ae=L.querySelector(".popupable-next-container"),re=L.querySelector(".popupable-prev-container");let ie,le,se,pe;const ce=!(navigator.maxTouchPoints>0||window.matchMedia("(hover: none)").matches);function A(){ce&&(clearTimeout(ie),pe||(ie=setTimeout(()=>{pe||(ae.classList.add("popupable-nav-inactive"),re.classList.add("popupable-nav-inactive"))},1500)))}function S(){ae.classList.remove("popupable-nav-inactive"),re.classList.remove("popupable-nav-inactive"),A()}async function Y(){const e=g[g.currentIndex];await e.ready,g.currentIndex?re.classList.remove("popupable-disabled"):re.classList.add("popupable-disabled"),g.currentIndex===g.length-1?ae.classList.add("popupable-disabled"):ae.classList.remove("popupable-disabled");for(const[e,t]of g.entries()){const n=e-g.currentIndex;t.cloneContainer.style.setProperty("--popupable-offset-multiplier",n),t.cloneContainer.style.zIndex=-1*Math.abs(n),t.content&&(n?n>0?(t.content.classList.add("popupable-content-after"),t.content.classList.remove("popupable-content-before")):(t.content.classList.add("popupable-content-before"),t.content.classList.remove("popupable-content-after")):(t.content.classList.remove("popupable-content-before"),t.content.classList.remove("popupable-content-after")))}if(e.id?y.id=e.id:y.removeAttribute("id"),P&&(P.textContent=`${g.currentIndex+1} / ${g.length}`),I){for(const[e,t]of I.entries())t.classList.toggle("popupable-thumbnail-active",e===g.currentIndex);const e=I[g.currentIndex];requestAnimationFrame(()=>{if(!e||!E?.isConnected)return;const t=getComputedStyle(E),n=parseFloat(t.paddingLeft)||0,o=parseFloat(t.paddingRight)||0,a=E.scrollLeft+n,r=E.scrollLeft+E.clientWidth-o,i=e.offsetLeft,l=i+e.offsetWidth;let s=E.scrollLeft;i<a?s=Math.max(0,i-n):l>r&&(s=l-E.clientWidth+o),s!==E.scrollLeft&&(M?E.scrollTo({left:s,behavior:"smooth"}):E.scrollLeft=s),M=!0})}updateExpandedSize()}if(z=()=>{g.currentIndex>=g.length-1||(g.currentIndex++,Y())},k=()=>{g.currentIndex<=0||(g.currentIndex--,Y())},Y(),ce&&A(),e.listeners.push({target:ae,event:"click",func:()=>z()},{target:re,event:"click",func:()=>k()},{target:document,event:"keydown",func:t=>{if("zoomed"!==e.state)switch(t.key){case"ArrowRight":case"ArrowDown":case"PageDown":case"d":case"s":z();break;case"ArrowLeft":case"ArrowUp":case"PageUp":case"a":case"w":k();break;case"Home":g.currentIndex=0,Y();break;case"End":g.currentIndex=g.length-1,Y();break;case"0":case"1":case"2":case"3":case"4":case"5":case"6":case"7":case"8":case"9":g.currentIndex=Math.min(Math.max(Number(t.key),1)-1,g.length-1),Y()}}},{target:document,event:"wheel",func:t=>{if("zoomed"===e.state)return;const n=performance.now();n-(T||0)<80||(t.deltaY>50?(T=n,z()):t.deltaY<-50&&(T=n,k()))},args:{passive:!0}}),E){let ue,de,fe,me,be,ve,he,ge,ye;function R(){ye&&(cancelAnimationFrame(ye),ye=null)}function B(){if(R(),Math.abs(ge)<.01)return;let e=performance.now();ye=requestAnimationFrame(function t(n){if(!E.isConnected)return void R();const o=E.scrollWidth-E.clientWidth;if(o<=0)return void R();const a=Math.min(32,n-e);e=n;let r=E.scrollLeft+ge*a;r<0&&(r=0),r>o&&(r=o),E.scrollLeft=r,E.scrollLeft<=.1&&ge<0||E.scrollLeft>=o-.1&&ge>0?R():(ge*=Math.pow(.8,a/16.67),Math.abs(ge)<=.002?R():ye=requestAnimationFrame(t))})}e.listeners.push({target:E,event:"pointerdown",func:e=>{if(0!==e.button)return;const t=E.scrollWidth-E.clientWidth;fe=t>0,R(),ue=!0,de=!1,me=e.clientX,be=E.scrollLeft,ve=E.scrollLeft,he=performance.now(),ge=0,fe&&E.classList.add("popupable-thumbnails-dragging"),E.setPointerCapture(e.pointerId)}},{target:E,event:"pointermove",func:e=>{if(!ue)return;const t=e.clientX-me;Math.abs(t)>3&&(de=!0);const n=performance.now(),o=n-he,a=be-t;if(E.scrollLeft=a,o>0){const e=(E.scrollLeft-ve)/o;ge=.65*ge+.35*e,ve=E.scrollLeft,he=n}}},{target:E,event:"pointerup",func:e=>{if(!ue)return;if(ue=!1,fe&&E.classList.remove("popupable-thumbnails-dragging"),E.hasPointerCapture(e.pointerId)&&E.releasePointerCapture(e.pointerId),de)return performance.now()-he>10&&(ge=0),void B();const t=document.elementFromPoint(e.clientX,e.clientY)?.closest?.(".popupable-thumbnail");t&&(g.currentIndex=Number(t.dataset.thumbnailIndex),Y())}},{target:E,event:"pointercancel",func:e=>{ue&&(ue=!1,fe&&E.classList.remove("popupable-thumbnails-dragging"),E.hasPointerCapture(e.pointerId)&&E.releasePointerCapture(e.pointerId),R())}},{target:E,event:"wheel",func:e=>{e.stopPropagation(),e.preventDefault();const t=E.scrollWidth-E.clientWidth;if(t<=0)return;const n=Math.abs(e.deltaX)>Math.abs(e.deltaY)?e.deltaX:e.deltaY,o=E.scrollLeft<=.1,a=E.scrollLeft>=t-.1;if(o&&n<0||a&&n>0)return;o&&n>0&&ge<0&&(ge=0),a&&n<0&&ge>0&&(ge=0);ge=(ge||0)+.015*n,ye||B()},args:{passive:!1}})}ce&&(e.listeners.push({target:ae,event:"pointerenter",func:()=>{pe=!0,S()}},{target:re,event:"pointerenter",func:()=>{pe=!0,S()}},{target:ae,event:"pointerleave",func:()=>{pe=!1,A()}},{target:re,event:"pointerleave",func:()=>{pe=!1,A()}}),e.listeners.push({target:y,event:"pointermove",func:t=>{if("zoomed"!==e.state)return null==le||null==se?(le=t.clientX,void(se=t.clientY)):void(Math.abs(t.clientX-le)<3&&Math.abs(t.clientY-se)<3||(le=t.clientX,se=t.clientY,S()))},args:{passive:!0}}));for(const Le of g)Le.content&&x&&x.append(Le.content)}else h&&x.append(h);const N={counter:!!P,content:!!x,thumbnails:!!E},F=m.order.top.filter(e=>N[e]),D=m.order.bottom.filter(e=>N[e]);function W(e,t){e&&("counter"===t&&P?(X[e===w?"counterTop":"counterBottom"]=!0,e.append(P)):"content"===t&&x?(X[e===w?"contentTop":"contentBottom"]=!0,e.append(x)):"thumbnails"===t&&E&&(X[e===w?"thumbnailsTop":"thumbnailsBottom"]=!0,e.append(E)))}F.length&&(w=document.createElement("div"),w.className="popupable-header"),D.length&&(C=document.createElement("div"),C.className="popupable-footer");for(const xe of F)W(w,xe);for(const we of D)W(C,we);if(w&&L.append(w),C&&L.append(C),y.append(f,L),Object.assign(d,m,{popup:y,group:g,contentContainer:x,thumbnailsContainer:E,orderPlacement:X,goNext:z,goPrev:k}),await d.ready,u!==o||e!==d||"close"===d.state)return;setCloneToOriginalRect(b,c),document.body.append(y),c.classList.add("popupable-hide"),disableScroll();const H=getComputedStyle(y);d.transition.duration=1e3*parseFloat(H.transitionDuration)+1e3*parseFloat(H.transitionDelay),y._state=d,requestAnimationFrame(()=>{requestAnimationFrame(()=>{openPopupable(y._state)})});let O,V=0;g&&y.addEventListener("dragstart",e=>e.preventDefault());const q=new Map;function $(e,t,n,o,r=[]){if("open"!==e.state)return;let i=0;const l=t.cloneContainer.parentElement,s=l.style.transform;if(s){const e=s.match(/translateX\((-?\d+(?:\.\d+)?)px\)/);e&&(i=Number(e[1])||0)}const p=Math.abs(i)>.5;a=!1,p?(t.cloneContainer.style.translate="0 0",t.cloneContainer.style.transition="translate var(--popupable-switch-duration), transform 0s",l.style.transition=null,l.style.transform=null,t.cloneContainer.style.translate=`${i}px 0`):(l.style.transition=null,l.style.transform=null),e.state="zoomed",y.classList.add("popupable-locked");let c,u,d=o;const f=new Map;let m,b,v,h,g,x,w,C,P,E,I=!1;const M=t.cloneContainer.getBoundingClientRect(),z=n?.clientX??M.left+M.width/2,k=n?.clientY??M.top+M.height/2;c=(z-M.left)*(1-d),u=(k-M.top)*(1-d);const T=()=>t.cloneContainer.style.transform=`translate(${c}px, ${u}px) scale(${d})`;function X(e,n,o){const a=d;var r;if(r=e,d=Math.min(6,Math.max(1,r)),d===a)return!1;const i=t.cloneContainer.getBoundingClientRect(),l=d/a,s=n-i.left,p=o-i.top;return c+=s*(1-l),u+=p*(1-l),!0}function A(){if(1===f.size){const e=f.values().next().value;return m=e.id,b=e.x,v=e.y,h=null,g=null,void(x=null)}if(f.size>=2){m=null;const[e,t]=[...f.values()];return h=(e.x+t.x)/2,g=(e.y+t.y)/2,void(x=Math.hypot(t.x-e.x,t.y-e.y))}m=null,b=null,v=null,h=null,g=null,x=null}if(t.cloneContainer.classList.add("popupable-zoomed"),T(),r.length){p||(t.cloneContainer.style.transition="none"),E=!0;for(const e of r)f.set(e.id,{id:e.id,x:e.x,y:e.y}),y.setPointerCapture(e.id);A()}e.unzoom=()=>{e.state="open",y.classList.remove("popupable-locked");for(const e of f.keys())y.hasPointerCapture(e)&&y.releasePointerCapture(e);f.clear(),t.cloneContainer.classList.remove("popupable-zoomed"),t.cloneContainer.style.transition=null,t.cloneContainer.style.transform=null,t.cloneContainer.style.translate=null;for(const t of e.zoomListeners)t.target.removeEventListener(t.event,t.func)},e.zoomListeners=[{target:y,event:"pointerdown",func:e=>{0===e.button&&(t.cloneContainer.style.transition="none",y.setPointerCapture(e.pointerId),f.set(e.pointerId,{id:e.pointerId,x:e.clientX,y:e.clientY}),1===f.size?(w=e.target,C=e.clientX,P=e.clientY,E=!1):E=!0,A(),e.preventDefault())}},{target:y,event:"pointermove",func:e=>{const t=f.get(e.pointerId);if(t){if(t.x=e.clientX,t.y=e.clientY,!E&&(Math.abs(e.clientX-C)>3||Math.abs(e.clientY-P)>3)&&(E=!0),1===f.size&&m===e.pointerId){const t=e.clientX-b,n=e.clientY-v;if(!t&&!n)return;return c+=t,u+=n,b=e.clientX,v=e.clientY,void T()}if(f.size>=2){const[e,t]=[...f.values()],n=(e.x+t.x)/2,o=(e.y+t.y)/2,a=Math.hypot(t.x-e.x,t.y-e.y);if(!x)return h=n,g=o,void(x=a);c+=n-h,u+=o-g,X(d*(a/x),n,o),I=!0,h=n,g=o,x=a,T()}}}},{target:y,event:"pointerup",func:n=>{if(f.has(n.pointerId)){if(f.delete(n.pointerId),y.hasPointerCapture(n.pointerId)&&y.releasePointerCapture(n.pointerId),I&&d<=1.01&&f.size<2)return e.skipOpenTouchPointerUps=f.size,void e.unzoom();if(!f.size&&!E&&Math.abs(n.clientX-C)<3&&Math.abs(n.clientY-P)<3){if(w?.closest?.(".popupable-clone-container")===t.cloneContainer||(w===y||w===L))return void e.unzoom()}A()}}},{target:y,event:"pointercancel",func:e=>{f.has(e.pointerId)&&(f.delete(e.pointerId),y.hasPointerCapture(e.pointerId)&&y.releasePointerCapture(e.pointerId),A())}},{target:y,event:"wheel",func:e=>{t.cloneContainer.style.transition="none",X(d*Math.exp(.002*-e.deltaY),e.clientX,e.clientY)&&T()},args:{passive:!0}}];for(const t of e.zoomListeners)t.target.addEventListener(t.event,t.func,t.args)}e.listeners.push({target:y,event:"pointerdown",func:e=>{if("open"!==y._state.state||"touch"!==e.pointerType)return;const t=y._state,n=t.group?t.group[t.group.currentIndex]:t;e.target.closest(".popupable-clone-container")===n.cloneContainer&&(q.set(e.pointerId,{id:e.pointerId,x:e.clientX,y:e.clientY}),q.size>=2&&($(t,n,e,1,[...q.values()].slice(0,2)),q.clear(),e.preventDefault()))}},{target:y,event:"pointermove",func:e=>{const t=q.get(e.pointerId);t&&(t.x=e.clientX,t.y=e.clientY)}},{target:y,event:"pointerup",func:e=>{q.delete(e.pointerId)}},{target:y,event:"pointercancel",func:e=>{q.delete(e.pointerId)}}),y.addEventListener("pointerup",t=>{if("zoomed"===y._state.state)return;if("touch"===t.pointerType&&l>1)return;if("touch"===t.pointerType&&(y._state.skipOpenTouchPointerUps||0)>0)return void y._state.skipOpenTouchPointerUps--;const o=performance.now(),a=null!=t.target.closest(".popupable-next-container, .popupable-prev-container");if(O&&o-V<250)return void(V=o);if(a?(O=!0,V=o):(O=!1,V=o),0!==t.button||!((t.target.classList.contains("popupable-clone")||t.target.classList.contains("popupable-clone-layer"))&&n.classList.contains("popupable-clone-container")||t.target==n&&(t.target.closest(".popupable-clone-container")||t.target.classList.contains("popupable-viewport")||t.target.classList.contains("popupable-container"))||t.target.classList.contains("popupable-container")&&n===e.original.parentElement))return;const r=y._state,i=r.group?r.group[r.group.currentIndex]:r;r.blocked&&(r.blocked=!1),"open"!==r.state?(t.stopPropagation(),e!==r&&(closePopupable(),e=r),openPopupable(e)):requestAnimationFrame(()=>{r.blocked?r.blocked=!1:i.zoomable&&(t.target.classList.contains("popupable-clone")||t.target.classList.contains("popupable-clone-layer"))?$(r,i,t,2):closePopupable()})})}),document.addEventListener("pointercancel",e=>{"touch"===e.pointerType&&(l=Math.max(0,l-1))}),document.addEventListener("keydown",t=>{if("Escape"===t.key||"Backspace"===t.key||" "===t.key||"Delete"===t.key){if("zoomed"===e.state)return void e.unzoom();closePopupable()}}),window.addEventListener("resize",updateExpandedSize),visualViewport&&visualViewport.addEventListener("resize",updateExpandedSize)}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "popupable",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight, zero-dependency lightbox library using modern JavaScript and CSS.",
|
|
5
|
+
"author": "Ewan Howell <ewanhowell5195>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/popupable.min.js",
|
|
9
|
+
"style": "./dist/popupable.min.css",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/popupable.min.js",
|
|
12
|
+
"./styles.css": "./dist/popupable.min.css"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/ewanhowell5195/popupable.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/ewanhowell5195/popupable/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/ewanhowell5195/popupable#readme",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "node build.js"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"clean-css": "^5.3.3",
|
|
30
|
+
"terser": "^5.43.1"
|
|
31
|
+
}
|
|
32
|
+
}
|