@thisiscrowd/crowdbox 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 +348 -0
- package/dist/crowdbox.css +1 -0
- package/dist/crowdbox.css.map +1 -0
- package/dist/crowdbox.esm.js +1557 -0
- package/dist/crowdbox.esm.js.map +1 -0
- package/dist/crowdbox.umd.js +8 -0
- package/dist/crowdbox.umd.js.map +1 -0
- package/package.json +59 -0
- package/src/adapters/iframe.js +30 -0
- package/src/adapters/image.js +91 -0
- package/src/adapters/video.js +44 -0
- package/src/adapters/vimeo.js +52 -0
- package/src/adapters/youtube.js +54 -0
- package/src/browser.js +7 -0
- package/src/core/Crowdbox.js +739 -0
- package/src/core/EventEmitter.js +43 -0
- package/src/core/LightboxGallery.js +2 -0
- package/src/core/Registry.js +26 -0
- package/src/core/utils.js +115 -0
- package/src/index.js +61 -0
- package/src/plugins/download/download.js +50 -0
- package/src/plugins/fullscreen/fullscreen.js +65 -0
- package/src/plugins/share/share.js +63 -0
- package/src/plugins/thumbs/thumbs.js +108 -0
- package/src/plugins/zoom/zoom.js +51 -0
- package/src/react/GalleryGrid.jsx +75 -0
- package/src/react/Lightbox.jsx +36 -0
- package/src/react/useLightbox.js +34 -0
- package/src/styles/_animations.scss +62 -0
- package/src/styles/_mosaic.scss +46 -0
- package/src/styles/_responsive.scss +34 -0
- package/src/styles/_thumbnails.scss +66 -0
- package/src/styles/_toolbar.scss +150 -0
- package/src/styles/_variables.scss +23 -0
- package/src/styles/main.scss +164 -0
- package/types/index.d.ts +239 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { Crowdbox } from '../core/Crowdbox.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Controlled React component wrapper around Crowdbox.
|
|
6
|
+
*
|
|
7
|
+
* Props:
|
|
8
|
+
* items GalleryItem[]
|
|
9
|
+
* open boolean
|
|
10
|
+
* startIndex number (default 0)
|
|
11
|
+
* onClose () => void
|
|
12
|
+
* + any CrowdboxOptions
|
|
13
|
+
*/
|
|
14
|
+
export function Lightbox({ items = [], open: isOpen, startIndex = 0, onClose, ...rest }) {
|
|
15
|
+
const galleryRef = useRef(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const g = new Crowdbox({ ...rest, selector: null });
|
|
19
|
+
g.on('close', () => onClose?.());
|
|
20
|
+
galleryRef.current = g;
|
|
21
|
+
return () => g.destroy();
|
|
22
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const g = galleryRef.current;
|
|
27
|
+
if (!g) return;
|
|
28
|
+
if (isOpen) {
|
|
29
|
+
g.open(items, startIndex);
|
|
30
|
+
} else {
|
|
31
|
+
g.close();
|
|
32
|
+
}
|
|
33
|
+
}, [isOpen, items, startIndex]);
|
|
34
|
+
|
|
35
|
+
return null; // renders into document.body via DOM API
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { Crowdbox } from '../core/Crowdbox.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook that creates and manages a Crowdbox instance.
|
|
6
|
+
*
|
|
7
|
+
* @param {import('../../types').CrowdboxOptions} options
|
|
8
|
+
* @returns {{ open, close, next, prev, goTo, gallery }}
|
|
9
|
+
*/
|
|
10
|
+
export function useLightbox(options = {}) {
|
|
11
|
+
const galleryRef = useRef(null);
|
|
12
|
+
const optsRef = useRef(options);
|
|
13
|
+
optsRef.current = options;
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const g = new Crowdbox(optsRef.current);
|
|
17
|
+
galleryRef.current = g;
|
|
18
|
+
return () => g.destroy();
|
|
19
|
+
}, []); // intentionally empty — recreate only on unmount
|
|
20
|
+
|
|
21
|
+
const open = useCallback((items, startIndex = 0) => {
|
|
22
|
+
galleryRef.current?.open(items, startIndex);
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const close = useCallback(() => {
|
|
26
|
+
galleryRef.current?.close();
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const next = useCallback(() => galleryRef.current?.next(), []);
|
|
30
|
+
const prev = useCallback(() => galleryRef.current?.prev(), []);
|
|
31
|
+
const goTo = useCallback((i) => galleryRef.current?.goTo(i), []);
|
|
32
|
+
|
|
33
|
+
return { open, close, next, prev, goTo, gallery: galleryRef };
|
|
34
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
@keyframes lgx-fade-in {
|
|
2
|
+
from { opacity: 0; }
|
|
3
|
+
to { opacity: 1; }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
@keyframes lgx-fade-out {
|
|
7
|
+
from { opacity: 1; }
|
|
8
|
+
to { opacity: 0; }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@keyframes lgx-slide-in-right {
|
|
12
|
+
from { transform: translate3d(32px, 0, 0) scale(0.98); opacity: 0; }
|
|
13
|
+
to { transform: translate3d(0, 0, 0) scale(1); opacity: 1; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@keyframes lgx-slide-in-left {
|
|
17
|
+
from { transform: translate3d(-32px, 0, 0) scale(0.98); opacity: 0; }
|
|
18
|
+
to { transform: translate3d(0, 0, 0) scale(1); opacity: 1; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes lgx-slide-out-left {
|
|
22
|
+
from { transform: translate3d(0, 0, 0) scale(1); opacity: 1; }
|
|
23
|
+
to { transform: translate3d(-32px, 0, 0) scale(0.98); opacity: 0; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@keyframes lgx-slide-out-right {
|
|
27
|
+
from { transform: translate3d(0, 0, 0) scale(1); opacity: 1; }
|
|
28
|
+
to { transform: translate3d(32px, 0, 0) scale(0.98); opacity: 0; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@keyframes lgx-spin {
|
|
32
|
+
to { transform: rotate(360deg); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@keyframes lgx-pulse {
|
|
36
|
+
0%, 100% { opacity: 1; }
|
|
37
|
+
50% { opacity: 0.4; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Slide transition classes applied by JS.
|
|
41
|
+
// All slides are already position:absolute (see .lgx-content in main.scss),
|
|
42
|
+
// so no position override needed here.
|
|
43
|
+
// "both" fill-mode on incoming slides ensures they start at opacity:0
|
|
44
|
+
// before the animation fires — eliminates the 1-frame flash at final position.
|
|
45
|
+
.lgx-slide--in-right,
|
|
46
|
+
.lgx-slide--in-left,
|
|
47
|
+
.lgx-slide--out-left,
|
|
48
|
+
.lgx-slide--out-right {
|
|
49
|
+
will-change: transform, opacity;
|
|
50
|
+
backface-visibility: hidden;
|
|
51
|
+
-webkit-backface-visibility: hidden;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.lgx-slide--active {
|
|
55
|
+
transform: translate3d(0, 0, 0) scale(1);
|
|
56
|
+
opacity: 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.lgx-slide--in-right { animation: lgx-slide-in-right var(--lgx-slide-duration) cubic-bezier(0.22, 1, 0.36, 1) both; }
|
|
60
|
+
.lgx-slide--in-left { animation: lgx-slide-in-left var(--lgx-slide-duration) cubic-bezier(0.22, 1, 0.36, 1) both; }
|
|
61
|
+
.lgx-slide--out-left { animation: lgx-slide-out-left var(--lgx-slide-duration) cubic-bezier(0.22, 1, 0.36, 1) forwards; }
|
|
62
|
+
.lgx-slide--out-right { animation: lgx-slide-out-right var(--lgx-slide-duration) cubic-bezier(0.22, 1, 0.36, 1) forwards; }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ─── Mosaic tile reveal animation ────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// When a high-res image loads, an 8×8 grid of tiles covers it.
|
|
4
|
+
// Tiles wave-dissolve away (diagonal top-left → bottom-right) revealing the image.
|
|
5
|
+
|
|
6
|
+
.lgx-mosaic {
|
|
7
|
+
position: absolute;
|
|
8
|
+
inset: 0;
|
|
9
|
+
display: grid;
|
|
10
|
+
pointer-events: none;
|
|
11
|
+
z-index: 3;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
border-radius: inherit;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.lgx-mosaic__tile {
|
|
17
|
+
--lgx-tile-delay: 0ms;
|
|
18
|
+
will-change: transform, opacity, border-radius;
|
|
19
|
+
animation: lgx-mosaic-out 420ms cubic-bezier(0.4, 0, 0.6, 1) var(--lgx-tile-delay, 0ms) both;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@keyframes lgx-mosaic-out {
|
|
23
|
+
0% {
|
|
24
|
+
transform: scale(1);
|
|
25
|
+
opacity: 1;
|
|
26
|
+
border-radius: 0;
|
|
27
|
+
}
|
|
28
|
+
45% {
|
|
29
|
+
transform: scale(1.06);
|
|
30
|
+
border-radius: 4px;
|
|
31
|
+
opacity: 1;
|
|
32
|
+
}
|
|
33
|
+
100% {
|
|
34
|
+
transform: scale(0) rotate(12deg);
|
|
35
|
+
border-radius: 50%;
|
|
36
|
+
opacity: 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Respect reduced-motion — instant removal with no animation
|
|
41
|
+
@media (prefers-reduced-motion: reduce) {
|
|
42
|
+
.lgx-mosaic__tile {
|
|
43
|
+
animation: none;
|
|
44
|
+
opacity: 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// ─── Responsive breakpoints ──────────────────────────────────────────────────
|
|
2
|
+
@media (max-width: 640px) {
|
|
3
|
+
:root {
|
|
4
|
+
--lgx-btn-size: 38px;
|
|
5
|
+
--lgx-nav-size: 42px;
|
|
6
|
+
--lgx-thumb-size: 56px;
|
|
7
|
+
--lgx-thumb-strip-h: 76px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.lgx-toolbar { padding: 6px 8px; gap: 4px; }
|
|
11
|
+
.lgx-btn--prev { left: 4px; }
|
|
12
|
+
.lgx-btn--next { right: 4px; }
|
|
13
|
+
.lgx-caption { font-size: 12px; padding: 8px 16px; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@media (hover: none) {
|
|
17
|
+
// Always show nav on touch devices
|
|
18
|
+
.lgx-btn--nav { opacity: 1 !important; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@media (prefers-reduced-motion: reduce) {
|
|
22
|
+
*, *::before, *::after {
|
|
23
|
+
animation-duration: 0.01ms !important;
|
|
24
|
+
animation-iteration-count: 1 !important;
|
|
25
|
+
transition-duration: 0.01ms !important;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// High contrast
|
|
30
|
+
@media (prefers-color-scheme: dark) {
|
|
31
|
+
:root {
|
|
32
|
+
--lgx-btn-bg: rgba(255, 255, 255, 0.15);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// ─── Thumbnail strip ─────────────────────────────────────────────────────────
|
|
2
|
+
.lgx-thumbs {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
gap: var(--lgx-thumb-gap);
|
|
7
|
+
overflow-x: auto;
|
|
8
|
+
overflow-y: hidden;
|
|
9
|
+
padding: 8px 12px;
|
|
10
|
+
height: var(--lgx-thumb-strip-h);
|
|
11
|
+
background: rgba(0, 0, 0, 0.5);
|
|
12
|
+
backdrop-filter: blur(4px);
|
|
13
|
+
scrollbar-width: thin;
|
|
14
|
+
scrollbar-color: rgba(255,255,255,.3) transparent;
|
|
15
|
+
flex-shrink: 0;
|
|
16
|
+
|
|
17
|
+
&::-webkit-scrollbar { height: 4px; }
|
|
18
|
+
&::-webkit-scrollbar-thumb { background: rgba(255,255,255,.3); border-radius: 2px; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.lgx-thumb {
|
|
22
|
+
flex-shrink: 0;
|
|
23
|
+
width: var(--lgx-thumb-size);
|
|
24
|
+
height: calc(var(--lgx-thumb-strip-h) - 20px);
|
|
25
|
+
border: 2px solid transparent;
|
|
26
|
+
border-radius: var(--lgx-radius);
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
padding: 0;
|
|
30
|
+
background: rgba(255,255,255,.08);
|
|
31
|
+
transition: border-color 0.2s var(--lgx-ease), opacity 0.2s var(--lgx-ease), transform 0.2s var(--lgx-ease);
|
|
32
|
+
opacity: 0.55;
|
|
33
|
+
|
|
34
|
+
img {
|
|
35
|
+
width: 100%;
|
|
36
|
+
height: 100%;
|
|
37
|
+
object-fit: cover;
|
|
38
|
+
display: block;
|
|
39
|
+
pointer-events: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&:hover {
|
|
43
|
+
opacity: 0.85;
|
|
44
|
+
transform: scale(1.05);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&--active {
|
|
48
|
+
border-color: var(--lgx-accent);
|
|
49
|
+
opacity: 1;
|
|
50
|
+
transform: scale(1.08);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&--no-image {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
color: var(--lgx-text-muted);
|
|
58
|
+
font-size: 13px;
|
|
59
|
+
font-weight: 600;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
&:focus-visible {
|
|
63
|
+
outline: 2px solid var(--lgx-accent);
|
|
64
|
+
outline-offset: 2px;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// ─── Toolbar ─────────────────────────────────────────────────────────────────
|
|
2
|
+
.lgx-toolbar {
|
|
3
|
+
position: absolute;
|
|
4
|
+
top: 0;
|
|
5
|
+
right: 0;
|
|
6
|
+
z-index: 10;
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 6px;
|
|
10
|
+
padding: 8px 12px;
|
|
11
|
+
background: linear-gradient(to bottom, rgba(0,0,0,.55) 0%, transparent 100%);
|
|
12
|
+
border-radius: 0 0 0 var(--lgx-radius);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── Buttons ─────────────────────────────────────────────────────────────────
|
|
16
|
+
.lgx-btn {
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
width: var(--lgx-btn-size);
|
|
21
|
+
height: var(--lgx-btn-size);
|
|
22
|
+
border: none;
|
|
23
|
+
border-radius: var(--lgx-btn-radius);
|
|
24
|
+
background: var(--lgx-btn-bg);
|
|
25
|
+
color: var(--lgx-text);
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
transition: background 0.18s ease, transform 0.15s ease, opacity 0.15s ease;
|
|
28
|
+
backdrop-filter: blur(6px);
|
|
29
|
+
-webkit-backdrop-filter: blur(6px);
|
|
30
|
+
|
|
31
|
+
svg { width: 20px; height: 20px; }
|
|
32
|
+
|
|
33
|
+
&:hover:not(:disabled) {
|
|
34
|
+
background: var(--lgx-btn-hover);
|
|
35
|
+
transform: scale(1.08);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&:active:not(:disabled) {
|
|
39
|
+
transform: scale(0.95);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&:disabled {
|
|
43
|
+
opacity: 0.3;
|
|
44
|
+
cursor: default;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&:focus-visible {
|
|
48
|
+
outline: 2px solid var(--lgx-accent);
|
|
49
|
+
outline-offset: 2px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&--close {
|
|
53
|
+
background: rgba(255,255,255,.15);
|
|
54
|
+
&:hover { background: rgba(220,60,60,.7); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&--active,
|
|
58
|
+
&--autoplay[aria-pressed='true'] {
|
|
59
|
+
background: #F5DA2E;
|
|
60
|
+
color: #000;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Navigation arrows ────────────────────────────────────────────────────────
|
|
65
|
+
// Buttons live inside .lgx-stage so top:50% is relative to the image area only.
|
|
66
|
+
.lgx-btn--nav {
|
|
67
|
+
position: absolute;
|
|
68
|
+
top: 50%;
|
|
69
|
+
transform: translateY(-50%);
|
|
70
|
+
width: var(--lgx-nav-size);
|
|
71
|
+
height: var(--lgx-nav-size);
|
|
72
|
+
border-radius: var(--lgx-btn-radius);
|
|
73
|
+
z-index: 10;
|
|
74
|
+
transition: background 0.18s ease, transform 0.15s ease, opacity 0.2s ease;
|
|
75
|
+
|
|
76
|
+
svg { width: 26px; height: 26px; }
|
|
77
|
+
|
|
78
|
+
&:hover:not(:disabled) { transform: translateY(-50%) scale(1.1); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.lgx-btn--prev { left: 12px; }
|
|
82
|
+
.lgx-btn--next { right: 12px; }
|
|
83
|
+
|
|
84
|
+
// ─── Counter & caption ────────────────────────────────────────────────────────
|
|
85
|
+
.lgx-counter {
|
|
86
|
+
position: absolute;
|
|
87
|
+
top: 16px;
|
|
88
|
+
left: 50%;
|
|
89
|
+
transform: translateX(-50%);
|
|
90
|
+
font-size: 13px;
|
|
91
|
+
font-weight: 600;
|
|
92
|
+
color: var(--lgx-text-muted);
|
|
93
|
+
letter-spacing: 0.5px;
|
|
94
|
+
pointer-events: none;
|
|
95
|
+
user-select: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.lgx-caption {
|
|
99
|
+
// Normal flex child — sits between .lgx-stage and .lgx-thumbs naturally.
|
|
100
|
+
// No z-index fighting, no fragile bottom offset calculation.
|
|
101
|
+
flex-shrink: 0;
|
|
102
|
+
padding: 10px 24px;
|
|
103
|
+
text-align: center;
|
|
104
|
+
font-size: 14px;
|
|
105
|
+
line-height: 1.5;
|
|
106
|
+
color: var(--lgx-text);
|
|
107
|
+
background: rgba(0, 0, 0, 0.55);
|
|
108
|
+
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Spinner ─────────────────────────────────────────────────────────────────
|
|
112
|
+
.lgx-spinner {
|
|
113
|
+
position: absolute;
|
|
114
|
+
inset: 0;
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
pointer-events: none;
|
|
119
|
+
|
|
120
|
+
&__ring {
|
|
121
|
+
width: 44px;
|
|
122
|
+
height: 44px;
|
|
123
|
+
border: 3px solid rgba(255,255,255,.15);
|
|
124
|
+
border-top-color: var(--lgx-text);
|
|
125
|
+
border-radius: 50%;
|
|
126
|
+
animation: lgx-spin 0.75s linear infinite;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Hide spinner once any descendant of the container has loaded.
|
|
131
|
+
// :has() is supported in all modern browsers (Chrome 105+, Safari 15.4+, Firefox 121+).
|
|
132
|
+
.lgx-container:has(.lgx-content--loaded) .lgx-spinner {
|
|
133
|
+
display: none;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Toast ───────────────────────────────────────────────────────────────────
|
|
137
|
+
.lgx-toast {
|
|
138
|
+
position: absolute;
|
|
139
|
+
bottom: 24px;
|
|
140
|
+
left: 50%;
|
|
141
|
+
transform: translateX(-50%);
|
|
142
|
+
padding: 8px 18px;
|
|
143
|
+
background: rgba(0,0,0,.75);
|
|
144
|
+
color: #fff;
|
|
145
|
+
font-size: 13px;
|
|
146
|
+
border-radius: 20px;
|
|
147
|
+
pointer-events: none;
|
|
148
|
+
animation: lgx-fade-in 0.2s ease, lgx-fade-out 0.3s 2.2s ease forwards;
|
|
149
|
+
white-space: nowrap;
|
|
150
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// ─── Design tokens ───────────────────────────────────────────────────────────
|
|
2
|
+
:root {
|
|
3
|
+
--lgx-backdrop: rgba(0, 0, 0, 0.92);
|
|
4
|
+
--lgx-bg: #111;
|
|
5
|
+
--lgx-text: #fff;
|
|
6
|
+
--lgx-text-muted: rgba(255, 255, 255, 0.6);
|
|
7
|
+
--lgx-accent: #4f8ef7;
|
|
8
|
+
--lgx-btn-bg: rgba(255, 255, 255, 0.12);
|
|
9
|
+
--lgx-btn-hover: rgba(255, 255, 255, 0.22);
|
|
10
|
+
--lgx-btn-size: 44px;
|
|
11
|
+
--lgx-btn-radius: 50%;
|
|
12
|
+
--lgx-nav-size: 54px;
|
|
13
|
+
--lgx-thumb-size: 72px;
|
|
14
|
+
--lgx-thumb-gap: 6px;
|
|
15
|
+
--lgx-toolbar-height: 56px;
|
|
16
|
+
--lgx-caption-height: 48px;
|
|
17
|
+
--lgx-thumb-strip-h: 90px;
|
|
18
|
+
--lgx-radius: 8px;
|
|
19
|
+
--lgx-z: 9999;
|
|
20
|
+
--lgx-duration: 300ms;
|
|
21
|
+
--lgx-ease: cubic-bezier(0.4, 0, 0.2, 1);
|
|
22
|
+
--lgx-slide-duration: 360ms;
|
|
23
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
@use 'variables';
|
|
2
|
+
@use 'animations';
|
|
3
|
+
@use 'toolbar';
|
|
4
|
+
@use 'thumbnails';
|
|
5
|
+
@use 'mosaic';
|
|
6
|
+
@use 'responsive';
|
|
7
|
+
|
|
8
|
+
// ─── Modal shell ─────────────────────────────────────────────────────────────
|
|
9
|
+
.lgx-modal {
|
|
10
|
+
position: fixed;
|
|
11
|
+
inset: 0;
|
|
12
|
+
z-index: var(--lgx-z);
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
16
|
+
color: var(--lgx-text);
|
|
17
|
+
-webkit-font-smoothing: antialiased;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.lgx-backdrop {
|
|
21
|
+
position: absolute;
|
|
22
|
+
inset: 0;
|
|
23
|
+
background: var(--lgx-backdrop);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.lgx-container {
|
|
27
|
+
position: relative;
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
flex: 1;
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
outline: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Stage ───────────────────────────────────────────────────────────────────
|
|
36
|
+
.lgx-stage {
|
|
37
|
+
position: relative;
|
|
38
|
+
flex: 1;
|
|
39
|
+
min-height: 0;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
user-select: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.lgx-slide-wrapper {
|
|
48
|
+
position: relative; // containing block for all absolutely-positioned slides
|
|
49
|
+
width: 100%;
|
|
50
|
+
height: 100%;
|
|
51
|
+
min-width: 0;
|
|
52
|
+
min-height: 0;
|
|
53
|
+
overflow: hidden; // clip slides that are mid-transition
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Slide content ────────────────────────────────────────────────────────────
|
|
57
|
+
// Every slide fills the wrapper absolutely so old + new can overlap during transition.
|
|
58
|
+
.lgx-content {
|
|
59
|
+
position: absolute;
|
|
60
|
+
inset: 0;
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
min-width: 0;
|
|
65
|
+
min-height: 0;
|
|
66
|
+
will-change: transform, opacity;
|
|
67
|
+
backface-visibility: hidden;
|
|
68
|
+
-webkit-backface-visibility: hidden;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Image-specific wrapper: needs overflow:hidden to clip mosaic tiles.
|
|
72
|
+
// position:absolute is inherited from .lgx-content.
|
|
73
|
+
.lgx-content--image {
|
|
74
|
+
display: grid;
|
|
75
|
+
place-items: center;
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
padding: clamp(16px, 4vh, 42px) clamp(16px, 5vw, 72px);
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.lgx-image {
|
|
82
|
+
max-width: 100%;
|
|
83
|
+
max-height: 100%;
|
|
84
|
+
width: auto;
|
|
85
|
+
height: auto;
|
|
86
|
+
margin: auto;
|
|
87
|
+
object-fit: contain;
|
|
88
|
+
display: block;
|
|
89
|
+
border-radius: 2px;
|
|
90
|
+
transform-origin: center center;
|
|
91
|
+
will-change: transform;
|
|
92
|
+
transition: transform 0.1s ease;
|
|
93
|
+
pointer-events: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Video ───────────────────────────────────────────────────────────────────
|
|
97
|
+
.lgx-video {
|
|
98
|
+
max-width: 90vw;
|
|
99
|
+
max-height: calc(90vh - var(--lgx-toolbar-height) - var(--lgx-caption-height) - var(--lgx-thumb-strip-h));
|
|
100
|
+
border-radius: var(--lgx-radius);
|
|
101
|
+
background: #000;
|
|
102
|
+
display: block;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── iframe / embed ──────────────────────────────────────────────────────────
|
|
106
|
+
.lgx-content--iframe {
|
|
107
|
+
padding: clamp(16px, 4vh, 42px) clamp(16px, 5vw, 72px);
|
|
108
|
+
box-sizing: border-box;
|
|
109
|
+
overflow: visible;
|
|
110
|
+
|
|
111
|
+
// Loading shimmer before iframe loads
|
|
112
|
+
&:not(.lgx-content--loaded)::after {
|
|
113
|
+
content: '';
|
|
114
|
+
position: absolute;
|
|
115
|
+
left: 50%;
|
|
116
|
+
top: 50%;
|
|
117
|
+
width: min(90vw, 960px);
|
|
118
|
+
height: min(56.25vw, 540px, calc(90vh - var(--lgx-toolbar-height) - var(--lgx-caption-height) - var(--lgx-thumb-strip-h) - 80px));
|
|
119
|
+
transform: translate(-50%, -50%);
|
|
120
|
+
border-radius: var(--lgx-radius);
|
|
121
|
+
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.04) 50%, transparent 100%);
|
|
122
|
+
background-size: 200% 100%;
|
|
123
|
+
animation: lgx-pulse 1.5s ease infinite;
|
|
124
|
+
pointer-events: none;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.lgx-iframe {
|
|
129
|
+
width: min(90vw, 960px);
|
|
130
|
+
height: min(56.25vw, 540px, calc(90vh - var(--lgx-toolbar-height) - var(--lgx-caption-height) - var(--lgx-thumb-strip-h) - 80px));
|
|
131
|
+
max-width: 100%;
|
|
132
|
+
max-height: 100%;
|
|
133
|
+
border: none;
|
|
134
|
+
display: block;
|
|
135
|
+
background: #000;
|
|
136
|
+
border-radius: var(--lgx-radius);
|
|
137
|
+
overflow: hidden;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Error state ─────────────────────────────────────────────────────────────
|
|
141
|
+
.lgx-content--error {
|
|
142
|
+
color: var(--lgx-text-muted);
|
|
143
|
+
font-size: 14px;
|
|
144
|
+
padding: 24px;
|
|
145
|
+
text-align: center;
|
|
146
|
+
|
|
147
|
+
&::before {
|
|
148
|
+
content: '⚠ Failed to load media';
|
|
149
|
+
display: block;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Fullscreen override ──────────────────────────────────────────────────────
|
|
154
|
+
.lgx-modal--fullscreen {
|
|
155
|
+
.lgx-image {
|
|
156
|
+
max-height: 100vh;
|
|
157
|
+
max-width: 100vw;
|
|
158
|
+
}
|
|
159
|
+
.lgx-iframe {
|
|
160
|
+
width: 100vw;
|
|
161
|
+
max-width: 100vw;
|
|
162
|
+
height: 100vh;
|
|
163
|
+
}
|
|
164
|
+
}
|