npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@shelamkoff/expose

v1.0.0

Published

Framework-agnostic fullscreen gallery with animations and composable plugins

Readme

@shelamkoff/expose

Независимая от UI-фреймворков полноэкранная галерея для изображений, видео, встроенных страниц и контента приложения. Она предоставляет асинхронную навигацию, именованные анимации, управляемую панель действий, типизированные события, управление фокусом и прокруткой, а также подключаемые плагины. Версия 1.0.0 поставляется как ESM-пакет для современных браузеров.

English README

Установка

npm install @shelamkoff/expose @shelamkoff/event-bus

Один раз подключите обязательные стили:

import '@shelamkoff/expose/styles.css'

Экспорт exposeStylesUrl позволяет приложению самостоятельно создать элемент <link>.

Быстрый старт

import {
  Expose,
  createCaptions,
  createFullscreen,
  createThumbnails,
  createZoom,
} from '@shelamkoff/expose'
import '@shelamkoff/expose/styles.css'

const gallery = new Expose([
  {
    src: '/photos/forest.jpg',
    thumb: '/photos/forest-thumb.jpg',
    alt: 'Лес',
    caption: 'Зимний лес',
  },
  {
    src: {
      type: 'video',
      url: '/video/trailer.mp4',
      poster: '/video/poster.jpg',
    },
  },
], {
  toolbar: ['counter'],
  counterFormat: '{current} / {total}',
  plugins: [
    createCaptions(),
    createThumbnails(),
    createZoom(),
    createFullscreen(),
  ],
})

const unsubscribe = gallery.on('slide:change', ({ index, slide }) => {
  console.log(index, slide.caption)
})

await gallery.open(0)

// При освобождении ресурсов:
unsubscribe()
await gallery.close()
gallery.destroy()

Методы open(), close(), next(), prev() и goTo() асинхронны, потому что ожидают завершения текущей анимации. Используйте await, если последующее состояние приложения зависит от завершения операции.

Источники слайда

interface SlideData {
  src: string | ImageSource | VideoSource | IFrameSource | RenderFunction
  caption?: string
  thumb?: string
  alt?: string
  download?: string | boolean
  preview?: string | (() => HTMLElement)
}

Изображение

const imageSlide = {
  src: {
    type: 'image',
    url: '/photo.jpg',
    srcset: '/photo-640.jpg 640w, /photo-1280.jpg 1280w',
    sizes: '100vw',
  },
  alt: 'Доступное описание',
}

Видео

const videoSlide = {
  src: {
    type: 'video',
    url: '/clip.mp4',
    autoplay: false,
    muted: false,
    loop: false,
    poster: '/poster.jpg',
  },
}

Встроенная страница

const iframeSlide = {
  src: {
    type: 'iframe',
    url: 'https://example.com/embed',
    allow: 'fullscreen',
    sandbox: 'allow-scripts allow-same-origin',
  },
}

Пользовательский рендеринг

const customSlide = {
  src: () => {
    const element = document.createElement('article')
    element.textContent = 'Контент приложения'

    return {
      element,
      destroy() {
        // Освободите ресурсы рендерера здесь.
      },
    }
  },
}

Тип строкового адреса определяется по расширению. Если адрес неоднозначен, передавайте объект с явным type. Функция рендеринга может вернуть HTMLElement или объект { element, destroy }.

Конфигурация

| Поле | Тип | По умолчанию | Назначение | | --- | --- | --- | --- | | loop | boolean | true | Переносит навигацию через границы списка. | | navigation | boolean | true | Показывает стрелки назад и вперёд. Клавиатура и жесты настраиваются отдельно. | | closeOnBackdrop | boolean | true | Закрывает галерею при нажатии на фон. | | animation | string | 'fade' | Имя зарегистрированной анимации. | | animationDuration | number | 300 | Неотрицательная длительность в миллисекундах. | | preload | number | 1 | Количество соседних слайдов для предварительной отрисовки с каждой стороны. | | startIndex | number | 0 | Начальный индекс, если open() вызван без аргумента. | | toolbar | ToolbarItem[] | [] | Счётчик 'counter' и пользовательские кнопки. | | counterFormat | string | '{current} / {total}' | Шаблон счётчика. | | plugins | ExposePlugin[] | [] | Плагины, устанавливаемые при создании. |

Некорректная конфигурация или структура слайда приводит к синхронному исключению при создании или изменении данных. Счётчик всегда прокручивается вверх, включая переход с последнего слайда на первый.

Публичный API

| Метод | Результат | Назначение | | --- | --- | --- | | Expose.registerAnimation(name, animation) | void | Регистрирует глобальную именованную анимацию. | | use(plugin) | this | Устанавливает плагин, пока галерея закрыта. | | getPlugin(name) | плагин или undefined | Возвращает публичный объект установленного плагина. | | open(index?) | Promise<void> | Создаёт и открывает оверлей. Для пустой галереи ничего не делает. | | close() | Promise<void> | Закрывает оверлей. Повторные вызовы используют уже выполняющуюся операцию. | | next() / prev() | Promise<void> | Переходит на один слайд, если галерея открыта и не анимируется. | | goTo(index) | Promise<void> | Переходит к допустимому индексу. | | getIndex() | number | Возвращает текущий индекс или -1, если слайд не выбран. | | getSlide() | слайд или null | Возвращает текущий слайд. | | getSlides() | SlideData[] | Возвращает защитную копию массива. | | setSlides(slides) | void | Заменяет все слайды. Пустой массив закрывает открытую галерею. | | addSlide(slide) | void | Добавляет слайд в конец. | | removeSlide(index) | void | Удаляет слайд, если переход не выполняется. Удаление последнего закрывает галерею. | | isOpen() | boolean | Возвращает состояние оверлея. | | on / off / once | подписка на события | Управляет обработчиками; on и once возвращают функции отписки. | | destroy() | void | Навсегда освобождает анимации, плагины, DOM оверлея, блокировку прокрутки и обработчики. |

Слайды разрешено менять в открытой галерее, кроме этапа закрытия и указанного ограничения removeSlide(). После destroy() экземпляр использовать нельзя.

Несколько галерей совместно используют блокировку прокрутки документа со счётчиком владельцев. Глобальные клавиши обрабатывает только верхняя открытая галерея. После закрытия библиотека по возможности восстанавливает фокус.

События

| Событие | Данные | | --- | --- | | open / open:complete | { index } | | close / close:complete | без данных | | slide:change | { index, slide } | | slide:load | { index, element } | | slides:change | { slides } | | zoom:change | { scale } | | fullscreen:change | { active } | | rotate | { rotation } | | flip | { flipH, flipV } | | autoplay:start / autoplay:stop | без данных | | destroy | без данных |

Кнопки панели действий

const gallery = new Expose(slides, {
  toolbar: [
    'counter',
    {
      name: 'copy-link',
      title: 'Копировать ссылку',
      icon: '<svg viewBox="0 0 24 24" aria-hidden="true">...</svg>',
      visible: slide => typeof slide.src === 'string',
      async onClick() {
        await navigator.clipboard.writeText(location.href)
      },
    },
  ],
})

Кнопка также может определять className, toggle, active и onStateChange(active). Имена кнопок должны быть уникальны. Строка icon вставляется как доверенный HTML.

Анимации

Анимация реализует enter, exit и transition. Каждый метод может вернуть промис и получает необязательный AbortSignal последним аргументом:

Expose.registerAnimation('instant', {
  enter(overlay) {
    overlay.style.opacity = '1'
  },
  exit(overlay) {
    overlay.style.opacity = '0'
  },
  transition(current, next) {
    current.hidden = true
    next.hidden = false
  },
})

При отмене сигнала остановите принадлежащие анимации таймеры и кадры. Исключение пользовательской анимации перехватывается, после чего галерея восстанавливает устойчивое визуальное состояние.

Встроенные плагины

Плагины разрешено устанавливать только при закрытой галерее. Один экземпляр плагина с состоянием может принадлежать только одной активной галерее.

Создание плагина

export function createSharePlugin() {
  let context = null

  return {
    name: 'share',

    install(pluginContext) {
      context = pluginContext
      pluginContext.toolbar.add({
        name: 'share',
        title: 'Поделиться',
        icon: '<svg viewBox="0 0 24 24" aria-hidden="true">...</svg>',
        visible: slide => pluginContext.resolveType(slide.src) === 'image',
        async onClick() {
          const slide = pluginContext.getSlide()
          const source = typeof slide?.src === 'object' ? slide.src.url : slide?.src
          if (typeof source === 'string' && navigator.share) {
            try {
              await navigator.share({ url: source })
            } catch {
              // Отмена пользователем ожидаема.
            }
          }
        },
      })
    },

    destroy() {
      context = null
    },
  }
}

Замороженный контекст плагина предоставляет подписки с автоматическим владением, асинхронную навигацию, состояние и настройки только для чтения, актуальные ссылки на DOM оверлея и слайда, управляемую регистрацию кнопок и resolveType(source). При ошибке установки и при уничтожении галереи подписки и кнопки удаляются автоматически. Глобальные обработчики, наблюдатели, таймеры, кадры, объектные URL, сторонние объекты и DOM за пределами управляемого оверлея остаются ответственностью плагина.

Граница безопасности

Адреса медиа проверяются до назначения DOM. Активные схемы отклоняются. Встроенные страницы допускают относительные, HTTP- и HTTPS-адреса, но не документы data: и blob:. Политика пользовательского sandbox остаётся ответственностью приложения. Функции рендеринга и HTML иконок считаются доверенным кодом разработчика; проверяйте или очищайте используемые там данные приложения.

Демо

Из каталога пакета:

npm install
npm run demo

Или из корня рабочего пространства:

npm run demo:expose

Откройте http://127.0.0.1:4173/expose/demo.html. Страница демонстрирует разные типы слайдов, анимации, кнопки панели и встроенные плагины.

Экспорты пакета

  • @shelamkoff/expose — JavaScript API и объявления TypeScript.
  • @shelamkoff/expose/styles.css — обязательные стили галереи.
  • @shelamkoff/expose/package.json — метаданные пакета.

Лицензия

MIT.