yet-another-react-lightbox 3.30.1 → 3.32.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/dist/index.js CHANGED
@@ -165,6 +165,7 @@ const LightboxDefaultProps = {
165
165
  closeOnPullUp: false,
166
166
  closeOnPullDown: false,
167
167
  closeOnBackdropClick: false,
168
+ closeOnEscape: true,
168
169
  preventDefaultWheelX: true,
169
170
  preventDefaultWheelY: false,
170
171
  disableSwipeNavigation: false,
@@ -1361,7 +1362,7 @@ function useKeyboardNavigation(subscribeSensors) {
1361
1362
  var _a;
1362
1363
  const isRTL = useRTL();
1363
1364
  const { publish } = useEvents();
1364
- const { animation } = useLightboxProps();
1365
+ const { animation, controller } = useLightboxProps();
1365
1366
  const { prevDisabled, nextDisabled } = useNavigationState();
1366
1367
  const throttle = ((_a = animation.navigation) !== null && _a !== void 0 ? _a : animation.swipe) / 2;
1367
1368
  const prev = useThrottle(() => publish(ACTION_PREV), throttle);
@@ -1369,6 +1370,8 @@ function useKeyboardNavigation(subscribeSensors) {
1369
1370
  const handleKeyDown = useEventCallback((event) => {
1370
1371
  switch (event.key) {
1371
1372
  case VK_ESCAPE:
1373
+ if (!controller.closeOnEscape)
1374
+ return;
1372
1375
  publish(ACTION_CLOSE);
1373
1376
  break;
1374
1377
  case VK_ARROW_LEFT:
@@ -1380,7 +1383,9 @@ function useKeyboardNavigation(subscribeSensors) {
1380
1383
  (isRTL ? prev : next)();
1381
1384
  break;
1382
1385
  default:
1386
+ return;
1383
1387
  }
1388
+ event.stopPropagation();
1384
1389
  });
1385
1390
  React.useEffect(() => subscribeSensors(EVENT_ON_KEY_DOWN, handleKeyDown), [subscribeSensors, handleKeyDown]);
1386
1391
  }
@@ -1541,7 +1546,7 @@ function Portal({ portal: { root, container: { className: containerClassName, st
1541
1546
  if (!restoreFocus.current) {
1542
1547
  restoreFocus.current = event.relatedTarget;
1543
1548
  }
1544
- }, ...containerRest }, children), root || document.body)
1549
+ }, ...containerRest }, children), (typeof root === "function" ? root() : root) || document.body)
1545
1550
  : null;
1546
1551
  }
1547
1552
  const PortalModule = createModule(MODULE_PORTAL, Portal);
@@ -32,6 +32,10 @@ declare module "yet-another-react-lightbox" {
32
32
  pinchZoomV4?: boolean;
33
33
  /** if `true`, enables image zoom via scroll gestures for mouse and trackpad users */
34
34
  scrollToZoom?: boolean;
35
+ /** custom slide types that support zoom */
36
+ supports?: readonly SlideTypeKey[];
37
+ /** maximum zoom level for custom slide types; when a function, return `undefined` to use the default (default: 8) */
38
+ maxZoom?: number | ((slide: Slide) => number | undefined);
35
39
  };
36
40
  }
37
41
  interface AnimationSettings {
@@ -14,6 +14,7 @@ const defaultZoomProps = {
14
14
  pinchZoomDistanceFactor: 100,
15
15
  pinchZoomV4: false,
16
16
  scrollToZoom: false,
17
+ maxZoom: 8,
17
18
  };
18
19
  function validateMinZoom(minZoom) {
19
20
  return Math.min(Math.max(minZoom, Number.EPSILON), 1);
@@ -77,13 +78,18 @@ function useZoomProps() {
77
78
  return resolveZoomProps(zoom);
78
79
  }
79
80
 
81
+ function resolveMaxZoom(maxZoom, slide) {
82
+ var _a;
83
+ const resolved = typeof maxZoom === "function" ? ((_a = maxZoom(slide)) !== null && _a !== void 0 ? _a : defaultZoomProps.maxZoom) : maxZoom;
84
+ return Math.max(resolved, 1);
85
+ }
80
86
  function useZoomImageRect(slideRect, imageDimensions) {
81
87
  var _a, _b;
82
88
  let imageRect = { width: 0, height: 0 };
83
89
  let maxImageRect = { width: 0, height: 0 };
84
90
  const { currentSlide } = useLightboxState();
85
91
  const { imageFit } = useLightboxProps().carousel;
86
- const { maxZoomPixelRatio } = useZoomProps();
92
+ const { maxZoomPixelRatio, maxZoom: maxZoomInProps } = useZoomProps();
87
93
  if (slideRect && currentSlide) {
88
94
  const slide = { ...currentSlide, ...imageDimensions };
89
95
  if (isImageSlide(slide)) {
@@ -112,8 +118,23 @@ function useZoomImageRect(slideRect, imageDimensions) {
112
118
  };
113
119
  }
114
120
  }
121
+ else if (slideRect.width > 0 && slideRect.height > 0) {
122
+ if (imageDimensions && imageDimensions.width > 0 && imageDimensions.height > 0) {
123
+ imageRect = {
124
+ width: Math.min(slideRect.width, imageDimensions.width),
125
+ height: Math.min(slideRect.height, imageDimensions.height),
126
+ };
127
+ }
128
+ else {
129
+ imageRect = { width: slideRect.width, height: slideRect.height };
130
+ }
131
+ }
115
132
  }
116
- const maxZoom = imageRect.width ? Math.max(round(maxImageRect.width / imageRect.width, 5), 1) : 1;
133
+ const maxZoom = currentSlide && imageRect.width
134
+ ? isImageSlide(currentSlide)
135
+ ? Math.max(round(maxImageRect.width / imageRect.width, 5), 1)
136
+ : resolveMaxZoom(maxZoomInProps, currentSlide)
137
+ : 1;
117
138
  return { imageRect, maxZoom };
118
139
  }
119
140
 
@@ -296,7 +317,7 @@ function useZoomState(imageRect, maxZoom, zoomWrapperRef) {
296
317
  const { containerRect, slideRect } = useController();
297
318
  const { minZoom, zoomInMultiplier } = useZoomProps();
298
319
  const currentSource = currentSlide && isImageSlide(currentSlide) ? currentSlide.src : undefined;
299
- const disabled = !currentSource || !(zoomWrapperRef === null || zoomWrapperRef === void 0 ? void 0 : zoomWrapperRef.current);
320
+ const disabled = !(zoomWrapperRef === null || zoomWrapperRef === void 0 ? void 0 : zoomWrapperRef.current);
300
321
  useLayoutEffect(() => {
301
322
  setZoom(1);
302
323
  setOffsetX(0);
@@ -468,10 +489,37 @@ function ZoomWrapper({ render, slide, offset, rect }) {
468
489
  var _a;
469
490
  const [imageDimensions, setImageDimensions] = React.useState();
470
491
  const zoomWrapperRef = React.useRef(null);
492
+ const isImage = isImageSlide(slide);
471
493
  const { zoom, maxZoom, offsetX, offsetY, setZoomWrapper } = useZoom();
472
494
  const interactive = zoom > 1;
473
495
  const { carousel, on } = useLightboxProps();
474
496
  const { currentIndex } = useLightboxState();
497
+ useLayoutEffect(() => {
498
+ if (offset !== 0 || isImage || !zoomWrapperRef.current)
499
+ return () => { };
500
+ const measure = () => {
501
+ const wrapper = zoomWrapperRef.current;
502
+ if (!wrapper)
503
+ return;
504
+ let width = 0;
505
+ let height = 0;
506
+ for (const child of wrapper.children) {
507
+ if (child instanceof HTMLElement) {
508
+ width = Math.max(width, child.offsetWidth);
509
+ height = Math.max(height, child.offsetHeight);
510
+ }
511
+ }
512
+ setImageDimensions((prev) => (prev && prev.width === width && prev.height === height ? prev : { width, height }));
513
+ };
514
+ measure();
515
+ if (typeof ResizeObserver === "undefined")
516
+ return () => { };
517
+ const observer = new ResizeObserver(measure);
518
+ for (const child of zoomWrapperRef.current.children) {
519
+ observer.observe(child);
520
+ }
521
+ return () => observer.disconnect();
522
+ }, [offset, isImage, rect]);
475
523
  useLayoutEffect(() => {
476
524
  if (offset === 0) {
477
525
  setZoomWrapper({ zoomWrapperRef, imageDimensions });
@@ -480,7 +528,7 @@ function ZoomWrapper({ render, slide, offset, rect }) {
480
528
  return () => { };
481
529
  }, [offset, imageDimensions, setZoomWrapper]);
482
530
  let rendered = (_a = render.slide) === null || _a === void 0 ? void 0 : _a.call(render, { slide, offset, rect, zoom, maxZoom });
483
- if (!rendered && isImageSlide(slide)) {
531
+ if (!rendered && isImage) {
484
532
  const slideProps = {
485
533
  slide,
486
534
  offset,
@@ -505,7 +553,10 @@ const Zoom = ({ augment, addModule }) => {
505
553
  toolbar: addToolbarButton(toolbar, PLUGIN_ZOOM, React.createElement(ZoomToolbarControl, null)),
506
554
  render: {
507
555
  ...render,
508
- slide: (props) => { var _a; return isImageSlide(props.slide) ? React.createElement(ZoomWrapper, { render: render, ...props }) : (_a = render.slide) === null || _a === void 0 ? void 0 : _a.call(render, props); },
556
+ slide: (props) => {
557
+ var _a, _b;
558
+ return isImageSlide(props.slide) || (props.slide.type != null && ((_a = zoom.supports) === null || _a === void 0 ? void 0 : _a.includes(props.slide.type))) ? (React.createElement(ZoomWrapper, { render: render, ...props })) : ((_b = render.slide) === null || _b === void 0 ? void 0 : _b.call(render, props));
559
+ },
509
560
  },
510
561
  controller: { ...controller, preventDefaultWheelY: zoom.scrollToZoom },
511
562
  ...restProps,
package/dist/styles.css CHANGED
@@ -1 +1 @@
1
- .yarl__fullsize{height:100%;width:100%}.yarl__relative{position:relative}.yarl__portal{bottom:0;left:0;opacity:0;overflow:hidden;position:fixed;right:0;top:0;transition:opacity var(--yarl__fade_animation_duration,.25s) var(--yarl__fade_animation_timing_function,ease);z-index:var(--yarl__portal_zindex,9999)}.yarl__portal_open{opacity:1}.yarl__container{background-color:var(--yarl__container_background_color,var(--yarl__color_backdrop,#000));bottom:0;left:0;outline:none;overflow:hidden;overscroll-behavior:var(--yarl__controller_overscroll_behavior,contain);position:absolute;right:0;top:0;touch-action:var(--yarl__controller_touch_action,none);-webkit-user-select:none;-moz-user-select:none;user-select:none}.yarl__carousel{align-content:center;align-items:stretch;display:flex;flex:0 0 auto;height:100%;justify-content:center;opacity:var(--yarl__pull_opacity,1);transform:translate(var(--yarl__swipe_offset,0),var(--yarl__pull_offset,0));width:calc(100% + (var(--yarl__carousel_slides_count) - 1)*(100% + var(--yarl__carousel_spacing_px, 0)*1px + var(--yarl__carousel_spacing_percent, 0)*1%))}.yarl__carousel_with_slides{-moz-column-gap:calc(var(--yarl__carousel_spacing_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_spacing_percent, 0)*1%);column-gap:calc(var(--yarl__carousel_spacing_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_spacing_percent, 0)*1%)}.yarl__flex_center{align-content:center;align-items:center;display:flex;justify-content:center}.yarl__slide{flex:1;overflow:hidden;padding:calc(var(--yarl__carousel_padding_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_padding_percent, 0)*1%);position:relative}[dir=rtl] .yarl__slide{--yarl__direction:-1}.yarl__slide_image{max-height:100%;max-width:100%;-o-object-fit:contain;object-fit:contain;touch-action:var(--yarl__controller_touch_action,none);-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-touch-callout:none}.yarl__slide_image_cover{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.yarl__slide_image_loading{opacity:0}@media screen and (min-width:800px){.yarl__slide_wrapper:not(.yarl__slide_wrapper_interactive) .yarl__slide_image{-webkit-backface-visibility:hidden;-webkit-transform:translateZ(0);-webkit-transform-style:preserve-3d}}.yarl__slide_placeholder{left:50%;line-height:0;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%)}.yarl__slide_loading{animation:yarl__delayed_fadein 1s linear;color:var(--yarl__slide_icon_loading_color,var(--yarl__color_button,hsla(0,0%,100%,.8)))}.yarl__slide_loading line{animation:yarl__stroke_opacity 1s linear infinite}.yarl__slide_loading line:first-of-type{animation-delay:-1.875s}.yarl__slide_loading line:nth-of-type(2){animation-delay:-1.75s}.yarl__slide_loading line:nth-of-type(3){animation-delay:-1.625s}.yarl__slide_loading line:nth-of-type(4){animation-delay:-1.5s}.yarl__slide_loading line:nth-of-type(5){animation-delay:-1.375s}.yarl__slide_loading line:nth-of-type(6){animation-delay:-1.25s}.yarl__slide_loading line:nth-of-type(7){animation-delay:-1.125s}.yarl__slide_loading line:nth-of-type(8){animation-delay:-1s}.yarl__slide_error{color:var(--yarl__slide_icon_error_color,red);height:var(--yarl__slide_icon_error_size,48px);width:var(--yarl__slide_icon_error_size,48px)}@media (prefers-reduced-motion){.yarl__portal,.yarl__slide{transition:unset}.yarl__slide_loading,.yarl__slide_loading line{animation:unset}}.yarl__toolbar{bottom:auto;display:flex;justify-content:flex-end;left:auto;padding:var(--yarl__toolbar_padding,8px);position:absolute;right:0;top:0}[dir=rtl] .yarl__toolbar{bottom:auto;left:0;right:auto;top:0}.yarl__icon{height:var(--yarl__icon_size,32px);width:var(--yarl__icon_size,32px)}.yarl__button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--yarl__button_background_color,transparent);border:var(--yarl__button_border,0);color:var(--yarl__color_button,hsla(0,0%,100%,.8));cursor:pointer;filter:var(--yarl__button_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));line-height:0;margin:var(--yarl__button_margin,0);outline:none;padding:var(--yarl__button_padding,8px);-webkit-tap-highlight-color:transparent}.yarl__button:focus{color:var(--yarl__color_button_active,#fff)}.yarl__button:focus:not(:focus-visible){color:var(--yarl__color_button,hsla(0,0%,100%,.8))}.yarl__button:focus-visible{color:var(--yarl__color_button_active,#fff)}@media (hover:hover){.yarl__button:focus-visible:hover,.yarl__button:focus:hover,.yarl__button:hover{color:var(--yarl__color_button_active,#fff)}}.yarl__button:disabled{color:var(--yarl__color_button_disabled,hsla(0,0%,100%,.4));cursor:default}.yarl__navigation_next,.yarl__navigation_prev{padding:var(--yarl__navigation_button_padding,24px 16px);position:absolute;top:50%;transform:translateY(-50%)}.yarl__navigation_prev{left:0}[dir=rtl] .yarl__navigation_prev{left:unset;right:0;transform:translateY(-50%) rotate(180deg)}.yarl__navigation_next{right:0}[dir=rtl] .yarl__navigation_next{left:0;right:unset;transform:translateY(-50%) rotate(180deg)}.yarl__no_scroll{height:100%;overflow:hidden;overscroll-behavior:none}@keyframes yarl__delayed_fadein{0%{opacity:0}80%{opacity:0}to{opacity:1}}@keyframes yarl__stroke_opacity{0%{stroke-opacity:1}to{stroke-opacity:.125}}
1
+ .yarl__fullsize{height:100%;width:100%}.yarl__relative{position:relative}.yarl__portal{bottom:0;left:0;opacity:0;overflow:hidden;pointer-events:auto;position:fixed;right:0;top:0;transition:opacity var(--yarl__fade_animation_duration,.25s) var(--yarl__fade_animation_timing_function,ease);z-index:var(--yarl__portal_zindex,9999)}.yarl__portal_open{opacity:1}.yarl__container{background-color:var(--yarl__container_background_color,var(--yarl__color_backdrop,#000));bottom:0;left:0;outline:none;overflow:hidden;overscroll-behavior:var(--yarl__controller_overscroll_behavior,contain);position:absolute;right:0;top:0;touch-action:var(--yarl__controller_touch_action,none);-webkit-user-select:none;-moz-user-select:none;user-select:none}.yarl__carousel{align-content:center;align-items:stretch;display:flex;flex:0 0 auto;height:100%;justify-content:center;opacity:var(--yarl__pull_opacity,1);transform:translate(var(--yarl__swipe_offset,0),var(--yarl__pull_offset,0));width:calc(100% + (var(--yarl__carousel_slides_count) - 1)*(100% + var(--yarl__carousel_spacing_px, 0)*1px + var(--yarl__carousel_spacing_percent, 0)*1%))}.yarl__carousel_with_slides{-moz-column-gap:calc(var(--yarl__carousel_spacing_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_spacing_percent, 0)*1%);column-gap:calc(var(--yarl__carousel_spacing_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_spacing_percent, 0)*1%)}.yarl__flex_center{align-content:center;align-items:center;display:flex;justify-content:center}.yarl__slide{flex:1;overflow:hidden;padding:calc(var(--yarl__carousel_padding_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_padding_percent, 0)*1%);position:relative}[dir=rtl] .yarl__slide{--yarl__direction:-1}.yarl__slide_image{max-height:100%;max-width:100%;-o-object-fit:contain;object-fit:contain;touch-action:var(--yarl__controller_touch_action,none);-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-touch-callout:none}.yarl__slide_image_cover{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.yarl__slide_image_loading{opacity:0}@media screen and (min-width:800px){.yarl__slide_wrapper:not(.yarl__slide_wrapper_interactive) .yarl__slide_image{-webkit-backface-visibility:hidden;-webkit-transform:translateZ(0);-webkit-transform-style:preserve-3d}}.yarl__slide_placeholder{left:50%;line-height:0;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%)}.yarl__slide_loading{animation:yarl__delayed_fadein 1s linear;color:var(--yarl__slide_icon_loading_color,var(--yarl__color_button,hsla(0,0%,100%,.8)))}.yarl__slide_loading line{animation:yarl__stroke_opacity 1s linear infinite}.yarl__slide_loading line:first-of-type{animation-delay:-1.875s}.yarl__slide_loading line:nth-of-type(2){animation-delay:-1.75s}.yarl__slide_loading line:nth-of-type(3){animation-delay:-1.625s}.yarl__slide_loading line:nth-of-type(4){animation-delay:-1.5s}.yarl__slide_loading line:nth-of-type(5){animation-delay:-1.375s}.yarl__slide_loading line:nth-of-type(6){animation-delay:-1.25s}.yarl__slide_loading line:nth-of-type(7){animation-delay:-1.125s}.yarl__slide_loading line:nth-of-type(8){animation-delay:-1s}.yarl__slide_error{color:var(--yarl__slide_icon_error_color,red);height:var(--yarl__slide_icon_error_size,48px);width:var(--yarl__slide_icon_error_size,48px)}@media (prefers-reduced-motion){.yarl__portal,.yarl__slide{transition:unset}.yarl__slide_loading,.yarl__slide_loading line{animation:unset}}.yarl__toolbar{bottom:auto;display:flex;justify-content:flex-end;left:auto;padding:var(--yarl__toolbar_padding,8px);position:absolute;right:0;top:0}[dir=rtl] .yarl__toolbar{bottom:auto;left:0;right:auto;top:0}.yarl__icon{height:var(--yarl__icon_size,32px);width:var(--yarl__icon_size,32px)}.yarl__button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--yarl__button_background_color,transparent);border:var(--yarl__button_border,0);color:var(--yarl__color_button,hsla(0,0%,100%,.8));cursor:pointer;filter:var(--yarl__button_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));line-height:0;margin:var(--yarl__button_margin,0);outline:none;padding:var(--yarl__button_padding,8px);-webkit-tap-highlight-color:transparent}.yarl__button:focus{color:var(--yarl__color_button_active,#fff)}.yarl__button:focus:not(:focus-visible){color:var(--yarl__color_button,hsla(0,0%,100%,.8))}.yarl__button:focus-visible{color:var(--yarl__color_button_active,#fff)}@media (hover:hover){.yarl__button:focus-visible:hover,.yarl__button:focus:hover,.yarl__button:hover{color:var(--yarl__color_button_active,#fff)}}.yarl__button:disabled{color:var(--yarl__color_button_disabled,hsla(0,0%,100%,.4));cursor:default}.yarl__navigation_next,.yarl__navigation_prev{padding:var(--yarl__navigation_button_padding,24px 16px);position:absolute;top:50%;transform:translateY(-50%)}.yarl__navigation_prev{left:0}[dir=rtl] .yarl__navigation_prev{left:unset;right:0;transform:translateY(-50%) rotate(180deg)}.yarl__navigation_next{right:0}[dir=rtl] .yarl__navigation_next{left:0;right:unset;transform:translateY(-50%) rotate(180deg)}.yarl__no_scroll{height:100%;overflow:hidden;overscroll-behavior:none}@keyframes yarl__delayed_fadein{0%{opacity:0}80%{opacity:0}to{opacity:1}}@keyframes yarl__stroke_opacity{0%{stroke-opacity:1}to{stroke-opacity:.125}}
package/dist/types.d.ts CHANGED
@@ -238,6 +238,8 @@ interface ControllerSettings {
238
238
  closeOnPullDown: boolean;
239
239
  /** if `true`, close the lightbox when the backdrop is clicked */
240
240
  closeOnBackdropClick: boolean;
241
+ /** if `true`, close the lightbox on Escape key press (default: `true`) */
242
+ closeOnEscape: boolean;
241
243
  /** if `true`, prevent default for horizontal wheel scroll events (for internal use only) */
242
244
  preventDefaultWheelX: boolean;
243
245
  /** if `true`, prevent default for vertical wheel scroll events (for internal use only) */
@@ -263,7 +265,7 @@ interface ControllerRef {
263
265
  /** Portal settings */
264
266
  interface PortalSettings {
265
267
  /** portal mount point */
266
- root?: DocumentFragment | Element | null;
268
+ root?: (() => DocumentFragment | Element | null) | DocumentFragment | Element | null;
267
269
  /** HTML attributes for the portal container */
268
270
  container?: React.HTMLAttributes<HTMLDivElement>;
269
271
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yet-another-react-lightbox",
3
- "version": "3.30.1",
3
+ "version": "3.32.0",
4
4
  "description": "Modern React lightbox component",
5
5
  "author": "Igor Danchenko",
6
6
  "license": "MIT",