MMV square buttons

- Use square cdx icon only buttons
- gray transparent brackground
- opaque background on hover, active, focus
- Some fixes to the positioning and offsets of buttons and dialogs
- Move the dialogs to be in the same container as the buttons
  This is better for accessibility but also fixes the z-index issue

Bug: T365192
Change-Id: Idbc2a309fbca15bd528aaed7ca9bed584487c4f3
This commit is contained in:
Derk-Jan Hartman 2024-06-13 00:59:08 +02:00
parent bdb40a8e04
commit 2a8b140ed3
13 changed files with 161 additions and 288 deletions

View file

@ -2,7 +2,7 @@
"modules": [
{
"resourceModule": "mmv",
"maxSize": "26.6 kB"
"maxSize": "26.4 kB"
},
{
"resourceModule": "mmv.ui.restriction",
@ -10,7 +10,7 @@
},
{
"resourceModule": "mmv.codex",
"maxSize": "5.1 kB"
"maxSize": "5.2 kB"
},
{
"resourceModule": "mmv.ui.reuse",

View file

@ -90,6 +90,7 @@
"mediawiki.user",
"mediawiki.util",
"mmv.bootstrap",
"mmv.codex",
"mmv.head"
],
"messages": [

View file

@ -55,12 +55,3 @@ body.mw-mmv-lightbox-open {
@param-size-icon: @size-icon-small );
}
}
.mw-mmv-button {
background-color: transparent;
min-width: 0;
border: 0;
padding: 0;
overflow-x: hidden;
text-indent: -9999em;
}

View file

@ -8,10 +8,11 @@
height: initial;
// positioned relative to the download button
position: fixed;
bottom: @metadatabar-above-fold-height + @progress-bar-height + 35px;
@bottom-offset: @metadatabar-above-fold-height + @buttons-offset-edge + @buttons-offset-each;
bottom: @bottom-offset - 5px;
.mw-mmv-dialog-down-arrow {
bottom: @metadatabar-above-fold-height + @progress-bar-height + 45px;
bottom: @bottom-offset + ( @buttons-size / 2 ) - ( @arrow-size / 2 );
background-color: @background-color-base;
}

View file

@ -11,10 +11,11 @@
// doesn't change when switching tabs
min-height: 300px;
// positioned relative to the reuse button
bottom: @metadatabar-above-fold-height + @progress-bar-height - 5px;
@bottom-offset: @metadatabar-above-fold-height + @buttons-offset-edge;
bottom: @bottom-offset - 5px;
.mw-mmv-dialog-down-arrow {
bottom: @metadatabar-above-fold-height + @progress-bar-height + 5px;
bottom: @bottom-offset + ( @buttons-size / 2 ) - ( @arrow-size / 2 );
}
.mw-mmv-dialog-warning {

View file

@ -121,16 +121,16 @@ class LightboxInterface extends UiElement {
/** @property {DialogProxy|ReuseDialog} */
this.fileReuse = new DialogProxy( 'mmv-reuse-open', ( req ) => {
const { ReuseDialog } = req( 'mmv.ui.reuse' );
this.fileReuse = new ReuseDialog( this.$innerWrapper, this.buttons.$download, this.config );
this.fileReuse = new ReuseDialog( this.$preDiv, this.buttons.$download, this.config );
return this.fileReuse;
} );
/** @property {DialogProxy|DownloadDialog} */
this.downloadDialog = new DialogProxy( 'mmv-download-open', ( req ) => {
const { DownloadDialog } = req( 'mmv.ui.reuse' );
this.downloadDialog = new DownloadDialog( this.$innerWrapper, this.buttons.$download, this.config );
this.downloadDialog = new DownloadDialog( this.$preDiv, this.buttons.$download, this.config );
return this.downloadDialog;
} );
this.optionsDialog = new OptionsDialog( this.$innerWrapper, this.buttons.$options, this.config );
this.optionsDialog = new OptionsDialog( this.$preDiv, this.buttons.$options, this.config );
}
/**
@ -151,9 +151,7 @@ class LightboxInterface extends UiElement {
*/
empty() {
this.panel.empty();
this.canvas.empty();
this.buttons.empty();
this.$main.addClass( 'metadata-panel-is-closed' )
@ -216,14 +214,7 @@ class LightboxInterface extends UiElement {
// mousemove generates a ton of events, which is why we throttle it
this.handleEvent( 'mousemove.lip', mw.util.throttle( ( e ) => {
this.mousemove( e );
}, 250 ) );
this.handleEvent( 'mmv-faded-out', ( e ) => {
this.fadedOut( e );
} );
this.handleEvent( 'mmv-fade-stopped', ( e ) => {
this.fadeStopped( e );
} );
}, 100, true ) );
this.buttons.connect( this, {
next: [ 'emit', 'next' ],
@ -233,7 +224,7 @@ class LightboxInterface extends UiElement {
const $parent = $( parentId || document.body );
// Clean up fullscreen data left attached to the DOM
this.$main.removeClass( 'jq-fullscreened' );
this.$main.removeClass( 'jq-fullscreened' ).removeClass( 'user-inactive' );
this.isFullscreen = false;
$parent
@ -258,19 +249,12 @@ class LightboxInterface extends UiElement {
this.panel.scroller.toggle( 'down' );
} );
// Buttons fading might not had been reset properly after a hard fullscreen exit
// This needs to happen after the parent attach() because the buttons need to be attached
// to the DOM for $.fn.stop() to work
this.buttons.stopFade();
this.buttons.attach();
this.fileReuse.attach();
this.downloadDialog.attach();
this.optionsDialog.attach();
// Reset the cursor fading
this.fadeStopped();
this.attached = true;
}
@ -334,6 +318,8 @@ class LightboxInterface extends UiElement {
}
this.isFullscreen = false;
this.$main.removeClass( 'jq-fullscreened' );
clearTimeout( this.interactionTimer );
this.userActivity();
}
/**
@ -346,6 +332,34 @@ class LightboxInterface extends UiElement {
}
this.isFullscreen = true;
this.$main.addClass( 'jq-fullscreened' );
this.resetInteractionTimer();
this.userInactive();
}
/**
* Interrupt and reset the 3sec delay to hide the controls
*/
resetInteractionTimer() {
clearTimeout( this.interactionTimer );
this.interactionTimer = setTimeout( () => {
this.userInactive();
}, 3000 );
}
/**
* In fullscreen, hide the mouse cursor and the controls
* Called from resetInteractionTimer()
*/
userInactive() {
this.$main.addClass( 'user-inactive' );
}
/**
* In fullscreen, show the mouse cursor and the controls
* Call this after any interactivity
*/
userActivity() {
this.$main.removeClass( 'user-inactive' );
}
/**
@ -353,29 +367,18 @@ class LightboxInterface extends UiElement {
*/
setupCanvasButtons() {
this.$closeButton = $( '<button>' )
.text( ' ' )
.addClass( 'mw-mmv-close' )
.addClass( 'cdx-button cdx-button--icon-only mw-mmv-button mw-mmv-close' )
.prop( 'title', mw.msg( 'multimediaviewer-close-popup-text' ) )
.on( 'click', () => {
this.unattach();
} );
this.$fullscreenButton = $( '<button>' )
.text( ' ' )
.addClass( 'mw-mmv-fullscreen' )
.addClass( 'cdx-button cdx-button--icon-only mw-mmv-button mw-mmv-fullscreen' )
.prop( 'title', mw.msg( 'multimediaviewer-fullscreen-popup-text' ) )
.on( 'click', ( e ) => {
.on( 'click', () => {
if ( this.isFullscreen ) {
this.exitFullscreen();
// mousemove is throttled and the mouse coordinates only
// register every 250ms, so there is a chance that we moved
// our mouse over one of the buttons but it didn't register,
// and a fadeOut is triggered; when we're coming back from
// fullscreen, we'll want to make sure the mouse data is
// current so that the fadeOut behavior will not trigger
this.mousePosition = { x: e.pageX, y: e.pageY };
this.buttons.revealAndFade( this.mousePosition );
} else {
this.enterFullscreen();
}
@ -412,14 +415,7 @@ class LightboxInterface extends UiElement {
}
if ( this.isFullscreen ) {
// When entering fullscreen without a mousemove, the browser
// still thinks that the cursor is where it was prior to entering
// fullscreen. I.e. on top of the fullscreen button
// Thus, we purposefully reset the saved position, so that
// the fade out really takes place (otherwise it's cancelled
// by updateControls which is called a few times when fullscreen opens)
this.mousePosition = { x: 0, y: 0 };
this.buttons.fadeOut();
this.userInactive();
}
// Some browsers only send resize events before toggling fullscreen, but not once the toggling is done
@ -450,6 +446,10 @@ class LightboxInterface extends UiElement {
} else if ( e.key === 'End' ) {
this.emit( 'last' );
e.preventDefault();
} else if ( this.isFullscreen ) {
// Any other key in fullscreen reveals the controls
this.resetInteractionTimer();
this.userActivity();
}
}
@ -469,35 +469,16 @@ class LightboxInterface extends UiElement {
return;
}
if ( e ) {
// Saving the mouse position is useful whenever we need to
// run LIP.mousemove manually, such as when going to the next/prev
// element
this.mousePosition = { x: e.pageX, y: e.pageY };
}
if ( this.isFullscreen ) {
this.buttons.revealAndFade( this.mousePosition );
this.resetInteractionTimer();
this.userActivity();
}
}
/**
* Called when the buttons have completely faded out and disappeared
*/
fadedOut() {
this.$main.addClass( 'cursor-hidden' );
}
/**
* Called when the buttons have stopped fading and are back into view
*/
fadeStopped() {
this.$main.removeClass( 'cursor-hidden' );
}
touchTap() {
if ( this.isFullscreen ) {
this.buttons.revealAndFade( this.mousePosition );
this.resetInteractionTimer();
this.userActivity();
}
}
@ -508,7 +489,7 @@ class LightboxInterface extends UiElement {
* @param {boolean} showNextButton Whether the next button should be revealed or not
*/
updateControls( showPrevButton, showNextButton ) {
const prevNextTop = `${ ( this.$imageWrapper.height() / 2 ) - 60 }px`;
const prevNextTop = `${ ( this.$imageWrapper.height() - 60 ) / 2 }px`;
if ( this.isFullscreen ) {
this.$postDiv.css( 'top', '' );

View file

@ -79,7 +79,7 @@
// Fullscreen styles
.cursor-hidden {
.user-inactive {
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
cursor: none;
}

View file

@ -24,6 +24,16 @@
@arrow-border-size: 2px;
// Some button things that get included all over
@navbutton-width: 18px;
@buttons-offset-right: 5px;
@buttons-offset-each-top: 37px;
@buttons-size: 32px;
@buttons-size-large: 44px;
@buttons-gap: 10px;
@buttons-offset-edge: 14px;
@buttons-offset-each: @buttons-size + @buttons-gap;
@buttons-icon-size: 20px; //icon size medium
@buttons-icon-size-large: 30px;
// dark mode version of --background-color-interactive-subtle
@buttons-background-color: #202122;
// dark mode version of --background-color-interactive-subtle: #202122;
@buttons-background-color-faded: ~'#20212274';

View file

@ -36,30 +36,26 @@ class CanvasButtons extends UiElement {
this.$reuse = $( '<a>' )
.attr( 'role', 'button' )
.addClass( 'mw-mmv-reuse-button' )
.text( '\u00A0' )
.addClass( 'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--icon-only mw-mmv-button mw-mmv-reuse-button' )
.prop( 'title', mw.msg( 'multimediaviewer-reuse-link' ) );
this.$options = $( '<button>' )
.text( ' ' )
.prop( 'title', mw.msg( 'multimediaviewer-options-tooltip' ) )
.addClass( 'mw-mmv-options-button' );
.addClass( 'cdx-button cdx-button--icon-only mw-mmv-button mw-mmv-options-button' );
this.$download = $( '<a>' )
.attr( 'role', 'button' )
.addClass( 'mw-mmv-download-button' )
.text( '\u00A0' )
.addClass( 'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--icon-only mw-mmv-button mw-mmv-download-button' )
.prop( 'title', mw.msg( 'multimediaviewer-download-link' ) );
this.$next = $( '<button>' )
.prop( 'title', mw.msg( 'multimediaviewer-next-image-alt-text' ) )
.addClass( 'mw-mmv-next-image disabled' )
.text( '\u00A0' );
.addClass( 'cdx-button cdx-button--icon-only cdx-button--size-large mw-mmv-button mw-mmv-next-image disabled' );
this.$prev = $( '<button>' )
.prop( 'title', mw.msg( 'multimediaviewer-prev-image-alt-text' ) )
.addClass( 'mw-mmv-prev-image disabled' )
.text( '\u00A0' );
.addClass( 'cdx-button cdx-button--icon-only cdx-button--size-large mw-mmv-button mw-mmv-prev-image disabled' );
this.$nav = this.$next
.add( this.$prev );
@ -102,18 +98,6 @@ class CanvasButtons extends UiElement {
} );
}
/**
* Stops the fading animation of the buttons and cancel any opacity value
*/
stopFade() {
this.$buttons
.stop( true )
.removeClass( 'hidden' )
.css( 'opacity', '' );
this.$container.trigger( $.Event( 'mmv-fade-stopped' ) );
}
/**
* Toggles buttons being disabled or not
*
@ -125,74 +109,6 @@ class CanvasButtons extends UiElement {
this.$prev.toggleClass( 'disabled', !showNextButton );
}
/**
* Fades out the active buttons
*/
fadeOut() {
// We don't use animation chaining because delay() can't be stop()ed
this.buttonsFadeTimeout = setTimeout( () => {
// FIXME: Use CSS transition
// eslint-disable-next-line no-jquery/no-animate
this.$buttons.not( '.disabled' ).animate( { opacity: 0 }, 1000, 'swing',
() => {
this.$buttons.addClass( 'hidden' );
this.$container.trigger( $.Event( 'mmv-faded-out' ) );
} );
}, 1500 );
}
/**
* Checks if any active buttons are currently hovered, given a position
*
* @param {number} x The horizontal coordinate of the position
* @param {number} y The vertical coordinate of the position
* @return {boolean}
*/
isAnyActiveButtonHovered( x, y ) {
// We don't use mouseenter/mouseleave events because content is subject
// to change underneath the cursor, eg. when entering fullscreen or
// when going prev/next (the button can disappear when reaching ends)
let hovered = false;
this.$buttons.not( '.disabled' ).each( ( idx, e ) => {
const $e = $( e );
const offset = $e.offset();
if ( y >= offset.top &&
// using css( 'height' ) & css( 'width' ) instead of .height()
// and .width() since those don't include padding, and as a
// result can return a smaller size than is actually the button
y <= offset.top + parseInt( $e.css( 'height' ) ) &&
x >= offset.left &&
x <= offset.left + parseInt( $e.css( 'width' ) ) ) {
hovered = true;
}
} );
return hovered;
}
/**
* Reveals all active buttons and schedule a fade out if needed
*
* @param {Object} [mousePosition] Mouse position containing 'x' and 'y' properties
*/
revealAndFade( mousePosition ) {
if ( this.buttonsFadeTimeout ) {
clearTimeout( this.buttonsFadeTimeout );
}
// Stop ongoing animations and make sure the buttons that need to be displayed are displayed
this.stopFade();
// mousePosition can be empty, for instance when we enter fullscreen and haven't
// recorded a real mousemove event yet
if ( !mousePosition ||
!this.isAnyActiveButtonHovered( mousePosition.x, mousePosition.y ) ) {
this.fadeOut();
}
}
/**
* Registers listeners.
*

View file

@ -3,129 +3,142 @@
@import 'mediawiki.mixins.less';
@import '../mmv.mixins.less';
.mw-mmv-download-button,
.mw-mmv-reuse-button,
.mw-mmv-options-button,
.mw-mmv-close,
.mw-mmv-fullscreen,
.mw-mmv-next-image,
.mw-mmv-prev-image {
cursor: pointer;
// Common to all buttons
// We override the codex background and borders to be their forced darkmode variant
// We use transparency on the background and on the icon color
.cdx-button.mw-mmv-button {
background-color: @buttons-background-color-faded;
position: fixed;
background-color: transparent;
background-repeat: no-repeat;
opacity: 0.8;
border: 0;
z-index: 1003;
// Cancel out default outline
border-width: 0;
outline: 0;
transition-property: background-color, opacity, border-width, border-color, box-shadow, transform;
&.mw-mmv-dialog-open,
&.mw-mmv-dialog-open, /* A button that has opened a dialog */
&:active,
&:hover {
text-decoration: none;
background-color: @buttons-background-color;
}
&:focus {
background-color: @buttons-background-color;
border-width: 1px;
}
&::after {
// Default transparency of the icon
content: '';
opacity: 0.8;
}
&.mw-mmv-dialog-open::after,
&:active::after,
&:focus::after,
&:hover::after {
transition: opacity 0.1s ease-in;
opacity: 1;
}
.user-select( none );
}
.mw-mmv-download-button.hidden,
.mw-mmv-reuse-button.hidden,
.mw-mmv-options-button.hidden,
.mw-mmv-close.hidden,
.mw-mmv-fullscreen.hidden,
.mw-mmv-next-image.hidden,
.mw-mmv-prev-image.hidden {
display: none;
}
.cursor-hidden {
.mw-mmv-download-button,
.mw-mmv-reuse-button,
.mw-mmv-close,
.mw-mmv-fullscreen,
.mw-mmv-next-image,
.mw-mmv-prev-image {
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
cursor: none;
// when the cursor is hidden, the buttons are hidden as well
.user-inactive & {
opacity: 0;
transform: scale( 0 );
}
}
// Positioning of buttons
.mw-mmv-download-button,
.mw-mmv-reuse-button,
.mw-mmv-options-button,
.mw-mmv-close,
.mw-mmv-fullscreen {
right: @buttons-offset-right;
right: @buttons-offset-edge;
left: auto;
transition: opacity 0.25s;
background-position: center;
margin-top: 14px;
width: 24px + 2 * 14px;
min-width: @buttons-size;
min-height: @buttons-size;
}
// Vertical positioning of left/right button
.mw-mmv-next-image,
.mw-mmv-prev-image {
top: -999px;
width: 80px;
height: 120px;
transition: opacity 0.25s, margin 0.25s;
&.disabled {
display: none;
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
cursor: none;
}
&&::after {
mask-size: @buttons-icon-size-large;
}
}
// Per button positioning and providing icons
.mw-mmv-close {
padding-right: @spacing-150;
top: @buttons-offset-right;
.cdx-mixin-css-icon( @cdx-icon-close, @param-fill-color: @color-inverted-fixed );
top: @buttons-offset-edge;
&::after {
.cdx-mixin-css-icon( @cdx-icon-close, @param-fill-color: @color-inverted-fixed );
}
}
.mw-mmv-fullscreen {
padding-right: @spacing-150;
top: ( @buttons-offset-right + ( @buttons-offset-each-top ) );
.cdx-mixin-css-icon( @cdx-icon-full-screen, @param-fill-color: @color-inverted-fixed );
top: ( @buttons-offset-edge + ( @buttons-offset-each ) );
&::after {
.cdx-mixin-css-icon( @cdx-icon-full-screen, @param-fill-color: @color-inverted-fixed );
}
}
.mw-mmv-options-button {
padding-right: @spacing-150;
top: ( @buttons-offset-right + ( 2 * @buttons-offset-each-top ) );
.cdx-mixin-css-icon( @cdx-icon-settings, @param-fill-color: @color-inverted-fixed );
top: ( @buttons-offset-edge + ( 2 * @buttons-offset-each ) );
&::after {
.cdx-mixin-css-icon( @cdx-icon-settings, @param-fill-color: @color-inverted-fixed );
}
}
.jq-fullscreened {
.mw-mmv-fullscreen {
.mw-mmv-fullscreen::after {
.cdx-mixin-css-icon( @cdx-icon-exit-fullscreen, @param-fill-color: @color-inverted-fixed );
}
}
.mw-mmv-next-image {
.cdx-mixin-css-icon( @cdx-icon-next, @param-fill-color: @color-inverted-fixed, @param-size-icon: @size-icon-medium );
right: @size-icon-medium;
&:hover {
margin-right: -4px;
&::after {
.cdx-mixin-css-icon( @cdx-icon-next, @param-fill-color: @color-inverted-fixed, @param-size-icon: @size-icon-medium );
}
}
.mw-mmv-prev-image {
.cdx-mixin-css-icon( @cdx-icon-previous, @param-fill-color: @color-inverted-fixed, @param-size-icon: @size-icon-medium );
left: @size-icon-medium;
&:hover {
margin-left: -4px;
&::after {
.cdx-mixin-css-icon( @cdx-icon-previous, @param-fill-color: @color-inverted-fixed, @param-size-icon: @size-icon-medium );
}
}
.mw-mmv-reuse-button {
padding-right: @spacing-150;
bottom: @buttons-offset-right + @metadatabar-above-fold-height + @progress-bar-height;
.cdx-mixin-css-icon( @cdx-icon-share, @param-fill-color: @color-inverted-fixed );
bottom: @buttons-offset-edge + @metadatabar-above-fold-height;
.jq-fullscreened & {
bottom: @buttons-offset-edge + @progress-bar-height;
}
&::after {
.cdx-mixin-css-icon( @cdx-icon-share, @param-fill-color: @color-inverted-fixed );
}
}
.mw-mmv-download-button {
padding-right: @spacing-150;
bottom: @buttons-offset-right + @metadatabar-above-fold-height + @progress-bar-height + 37px;
.cdx-mixin-css-icon( @cdx-icon-download, @param-fill-color: @color-inverted-fixed );
bottom: @buttons-offset-edge + @metadatabar-above-fold-height + @buttons-offset-each;
.jq-fullscreened & {
bottom: @buttons-offset-edge + @progress-bar-height + @buttons-offset-each;
}
&::after {
.cdx-mixin-css-icon( @cdx-icon-download, @param-fill-color: @color-inverted-fixed );
}
}

View file

@ -9,7 +9,7 @@
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
.mw-mmv-dialog {
position: fixed;
right: 58px;
right: @buttons-offset-edge + @buttons-offset-each + ( @arrow-size / 2 );
display: none;
width: @dialog-width;
height: @dialog-height;
@ -18,7 +18,7 @@
z-index: 1004;
.mw-mmv-dialog-down-arrow {
right: 48px;
right: @buttons-offset-edge + @buttons-offset-each;
background-color: @background-color;
width: @arrow-size;
height: @arrow-size;

View file

@ -3,7 +3,7 @@
@import '../mmv.mixins.less';
.mw-mmv-options-dialog {
@offset-top: ( @buttons-offset-right + ( 2 * @buttons-offset-each-top ) + 6px );
@offset-top: @buttons-offset-edge + ( 2 * @buttons-offset-each );
top: @offset-top;
height: auto;
z-index: 1004;

View file

@ -213,47 +213,6 @@ const { getMultimediaViewer } = require( './mmv.testhelpers.js' );
restoreScrollTo();
} );
QUnit.test( 'isAnyActiveButtonHovered', ( assert ) => {
const lightbox = new LightboxInterface();
stubScrollTo();
// Attach lightbox to testing fixture to avoid interference with other tests.
lightbox.attach( '#qunit-fixture' );
lightbox.buttons.$buttons.each( ( i, button ) => {
const $button = $( button );
const offset = $button.show().offset();
const width = $button.width();
const height = $button.height();
const disabled = $button.hasClass( 'disabled' );
assert.strictEqual( lightbox.buttons.isAnyActiveButtonHovered( offset.left, offset.top ),
!disabled,
'Hover detection works for top-left corner of element' );
assert.strictEqual( lightbox.buttons.isAnyActiveButtonHovered( offset.left + width, offset.top ),
!disabled,
'Hover detection works for top-right corner of element' );
assert.strictEqual( lightbox.buttons.isAnyActiveButtonHovered( offset.left, offset.top + height ),
!disabled,
'Hover detection works for bottom-left corner of element' );
assert.strictEqual( lightbox.buttons.isAnyActiveButtonHovered( offset.left + width, offset.top + height ),
!disabled,
'Hover detection works for bottom-right corner of element' );
assert.strictEqual(
lightbox.buttons.isAnyActiveButtonHovered(
offset.left + ( width / 2 ), offset.top + ( height / 2 )
),
!disabled,
'Hover detection works for center of element'
);
} );
// Unattach lightbox from document
lightbox.unattach();
restoreScrollTo();
} );
QUnit.test( 'Keyboard prev/next', ( assert ) => {
const viewer = getMultimediaViewer();
const lightbox = new LightboxInterface();