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

@zyplai/support-widget

v0.1.10

Published

Embeddable support-ticket widget (Preact + Vite, Shadow DOM, single IIFE bundle).

Readme

@zyplai/support-widget

Встраиваемый виджет тикетов поддержки. Один IIFE-бандл, без зависимостей у хост-приложения, изолированные стили через Shadow DOM. Собирается из этого репо, публикуется на npm, раздаётся через jsDelivr.

Возможности

  • Изоляция стилей — рендер внутри Shadow DOM, никаких конфликтов с CSS хоста (включая Tailwind / Bootstrap / любой reset).
  • Нет зависимостей у хоста — Preact + Tailwind зашиты внутрь IIFE, хосту достаточно одного <script>.
  • i18nen, ru, ar (с автоматическим RTL для арабского).
  • Очередь до загрузки — вызовы window.fw(...) сделанные до того, как скрипт догрузился, замораживаются и проигрываются после mount'а.
  • Late-binding авторизации — токен и baseUrl читаются интерсептором на каждом запросе, можно переинициализировать виджет в рантайме (например, после рефреша JWT).
  • Хоткей ⌘J / Ctrl+J — открывает/закрывает панель.
  • Идемпотентный mount — повторный init() обновит конфиг, но не сделает remount.

Установка через CDN

<!-- pin exact (рекомендуется для прода) -->
<script async src="https://cdn.jsdelivr.net/npm/@zyplai/[email protected]/dist/widget.iife.js"></script>

Альтернативные шаблоны URL — см. Версионирование CDN.


Подключение за 3 шага (vanilla HTML)

Шаг 1 — queue-stub до подключения скрипта

В <head> или сразу перед загрузкой виджета:

<script>
  window.fw = window.fw || function () {
    (window.fw.q = window.fw.q || []).push(arguments);
  };
</script>

Этот stub нужен, чтобы можно было вызывать window.fw(...) ещё до того, как сам бандл загрузился. Все вызовы попадут в очередь и выполнятся, как только IIFE отработает.

Шаг 2 — подключить виджет

<script async src="https://cdn.jsdelivr.net/npm/@zyplai/[email protected]/dist/widget.iife.js"></script>

async безопасен — очередь из шага 1 гарантирует, что ни один init/open не потеряется.

Шаг 3 — init с авторизационным контекстом

window.fw('init', {
  token:     'eyJhbGciOi...',                            // обязательно: JWT или "Bearer <jwt>"
  partnerId: 42,                                         // обязательно: integer или string
  baseUrl:   'https://api.example.com',                  // опционально — см. ниже
  lang:      'ru',                                       // опционально — 'en' | 'ru' | 'ar'
  hasAccess: true,                                       // опционально: управление доступом (по умолчанию true)
});

Без token и partnerId виджет не смонтируется, в консоль уйдёт [fw] init() requires token and partnerId.


Интеграция в React + Vite (TS)

1. index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Your app</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>

    <script>
      window.fw = window.fw || function () {
        (window.fw.q = window.fw.q || []).push(arguments);
      };
    </script>
    <script async src="https://cdn.jsdelivr.net/npm/@zyplai/[email protected]/dist/widget.iife.js"></script>
  </body>
</html>

2. Типы window.fw для TS

Без этих типов tsc упадёт при использовании window.fw?.(...). Добавь в src/vite-env.d.ts:

/// <reference types="vite/client" />

type FwLang = "en" | "ru" | "ar";

interface FwInitConfig {
  token: string;
  partnerId: number | string;
  baseUrl?: string;
  lang?: FwLang;
  hasAccess?: boolean;
}

interface FwApi {
  (cmd: "init", config: FwInitConfig): void;
  (cmd: "open" | "close" | "toggle"): void;
  (cmd: "setLanguage", lang: FwLang | string): void;
  (cmd: "setAccess", hasAccess: boolean): void;
  (cmd: "destroy"): void;
  q?: IArguments[];
  onLanguageChange?: (cb: (lang: FwLang) => void) => () => void;
  getLanguage?: () => FwLang;
}

interface Window {
  fw?: FwApi;
}

3. Инициализация после логина

Виджет нужно инициализировать, когда уже есть token и partnerId (обычно — после успешного логина и записи их в sessionStorage/store). Готовый хук:

// src/pages/<...>/lib/useInitSupportWidget.ts
import { useEffect } from "react";

export function useInitSupportWidget() {
  useEffect(() => {
    const token     = sessionStorage.getItem("token");
    const partnerId = sessionStorage.getItem("partnerId");
    const lang      = sessionStorage.getItem("language") ?? "en";

    if (!token || !partnerId) return;

    window.fw?.("init", {
      token,
      partnerId: Number(partnerId),
      lang: lang as "en" | "ru" | "ar",
    });
  }, []);
}

Вызывай хук в верхнем компоненте под приватным роутом — там, где гарантированно есть auth-контекст (например, в _layout.tsx / AuthenticatedShell.tsx).

4. Синхронизация языка приложения и виджета

Если у тебя свой LanguageContext/i18n, прокидывай язык в виджет при каждой смене:

const changeLanguage = (lang: FwLang) => {
  window.fw?.("setLanguage", lang);     // ← синхронизируем виджет
  i18n.changeLanguage(lang);
  setCurrentLanguage(lang);
  sessionStorage.setItem("language", lang);
};

Подробнее про управление языком — раздел Управление языком.


Init-конфиг

| Поле | Тип | Обязательно | Поведение | |-------------|------------------------------|-------------|--------------------------------------------------------------------------------------------| | token | string | да | JWT. Принимается как "<jwt>", так и "Bearer <jwt>" — префикс срезается автоматически. | | partnerId | number \| string | да | Идентификатор партнёра. Используется в URL запросов: /partner/{partnerId}/.... | | baseUrl | string | условно | Базовый URL API. Можно пропустить, если бандл собран с VITE_API_BASE_URL (см. ниже). | | lang | 'en' \| 'ru' \| 'ar' | нет | Стартовый язык. По умолчанию — детект из navigator.language, fallback 'en'. | | hasAccess | boolean | нет | Управление доступом к виджету. true (по умолчанию) — виджет видимый и функционален. false — виджет не рендерится. |

baseUrl: build-time vs init-time

  • Build-time — если виджет собирается с VITE_API_BASE_URL=https://... в окружении, этот URL запекается в бандл, и хост-приложение может не передавать baseUrl в init().
  • Init-time — если VITE_API_BASE_URL не задан на сборке, baseUrl обязателен в init(). Без него виджет не смонтируется и в консоль уйдёт [fw] Widget not mounted: baseUrl is not configured.
  • Значение из init() всегда перекрывает build-time дефолт.

Повторный init()

Повторный вызов init() обновит конфиг (новый токен/partnerId/baseUrl), но не делает remount — DOM-узел остаётся прежним, виджет продолжает работать. Удобно для рефреша JWT.


Команды window.fw

| Команда | Описание | |-------------------------------|-------------------------------------------| | fw('init', config) | Настроить API и смонтировать виджет | | fw('open') | Открыть панель | | fw('close') | Закрыть панель | | fw('toggle') | Переключить open/close | | fw('setLanguage', lang) | Сменить язык ('en' / 'ru' / 'ar') | | fw('setAccess', hasAccess) | Управлять доступом к виджету (true / false) | | fw('destroy') | Полностью удалить виджет из DOM и очистить подписки |

Все вызовы, сделанные до того как скрипт догрузился, попадают в очередь window.fw.q и проигрываются автоматически — порядок сохраняется.


Управление языком

Три способа взаимодействия:

// 1) Программная смена (триггерит и виджет, и подписчиков)
window.fw('setLanguage', 'ru');

// 2) Подписка на смену (возвращает функцию unsubscribe)
const unsubscribe = window.fw.onLanguageChange((lang) => {
  console.log('language is now', lang);
});
unsubscribe();

// 3) Подписка через DOM-событие
window.addEventListener('fw:languageChanged', (e) => {
  console.log(e.detail.lang);     // 'en' | 'ru' | 'ar'
});

// Текущий язык
const lang = window.fw.getLanguage();

Стартовое определение: navigator.language.split('-')[0]'ru' / 'ar' распознаются, всё остальное → 'en'. Передача lang в init() перекрывает детект.


Управление доступом к виджету

Виджет поддерживает динамическое управление доступом через параметр hasAccess. Когда hasAccess: false, виджет полностью скрывается и не рендерится:

// 1) При инициализации
window.fw('init', {
  token: '...',
  partnerId: 42,
  hasAccess: false,   // ← виджет скрыт до изменения
});

// 2) Изменить доступ в рантайме
window.fw('setAccess', true);   // ← виджет теперь виден и функционален
window.fw('setAccess', false);  // ← виджет скрыт снова

Типичный сценарий: скрыть виджет для пользователей на бесплатном тарифе, включить для платных подписок:

const userTier = await getCurrentUserTier();
window.fw?.('setAccess', userTier === 'premium');

Поведение и lifecycle

  • Mount target: при первом init() создаётся <div id="feedback-widget-host"> в document.body, к нему подключается Shadow DOM (mode: 'open'), внутри инжектится <style> со скомпилированным Tailwind. Никакие селекторы хоста туда не дотянутся.
  • Radix-порталы (dropdown-меню, диалоги) рендерятся внутрь shadow-root через PortalContainerContext — стилевая изоляция сохраняется.
  • Guard'ы:
    • нет token или partnerIdinit отклоняется, лог [fw] init() requires token and partnerId
    • нет baseUrl (ни в build, ни в init) → mount отменяется, лог [fw] Widget not mounted: baseUrl is not configured
  • Хоткей ⌘J / Ctrl+J — кроссплатформенный (metaKey || ctrlKey), preventDefault срабатывает только если виджет уже смонтирован.
  • RTL: при lang === 'ar' shadow-root получает атрибут dir="rtl". Tailwind-классы используют логические свойства (ps-*, pe-*, start-*, end-*), так что зеркалирование работает само.
  • Очистка: команда destroy полностью удаляет виджет из DOM, отписывает все слушатели и сбрасывает внутреннее состояние. После этого можно снова вызвать init() для переинициализации.

Контракт с бэкендом

Виджет ходит исключительно по эндпоинтам тикетинга:

| Метод | Путь | |---------|--------------------------------------------------------------------| | GET | /partner/{partnerId}/notion_ticketing/tickets | | GET | /partner/{partnerId}/notion_ticketing/tickets/{ticketId} | | POST | /partner/{partnerId}/notion_ticketing/tickets (multipart/form-data) | | PATCH | /partner/{partnerId}/notion_ticketing/tickets/{ticketId} | | DELETE| /partner/{partnerId}/notion_ticketing/tickets/{ticketId} |

  • Базовый URL — apiConfig.baseUrl (build-time или init-time, см. выше).
  • Заголовок Authorization: Bearer <token> добавляется на каждый запрос — токен читается из apiConfig в момент запроса, поэтому переинициализация виджета сразу же подхватывается.
  • Timeout: 15 секунд.
  • Ошибки нормализуются в ApiError(status, message, data) — для хоста наружу не торчат, но видны в DevTools при сетевых сбоях.

Создание тикета (POST)

multipart/form-data, поля:

  • payload — JSON-строка с { subject, priority, status, category, problem }
  • attachments — повторяющийся File (опционально, до 5 файлов, до 25 МБ каждый; форматы PNG/JPG/MP4/WEBM/PDF — лимиты проверяются на клиенте)

Справочник: статусы и приоритеты

Используются при отображении и фильтрации тикетов.

Статусы:

To do, BackLog, In progress, In review, Testing, Bug, On hold, Completed, Archived

Открытыми считаются: To do, BackLog, In progress, In review, Testing, Bug, On hold (вкладка «Open» в виджете фильтрует по этому списку).

Приоритеты:

Low, Medium, High, Super High

Версионирование CDN URL

| Шаблон | Когда использовать | |------------------------------------------------------------------------------------|-------------------------------------------------------------| | https://cdn.jsdelivr.net/npm/@zyplai/[email protected]/dist/widget.iife.js | Прод. Immutable-кэш на год, версия зафиксирована. | | https://cdn.jsdelivr.net/npm/@zyplai/[email protected]/dist/widget.iife.js | Авто-патчи внутри минора. Можно для прода, если доверяешь semver. | | https://cdn.jsdelivr.net/npm/@zyplai/support-widget@latest/dist/widget.iife.js | Только dev/demo. Может сломаться при мажоре. |

Сменить версию = поменять число в <script src=...>. Никаких пересборок хоста не требуется.


Локальная разработка

npm install
npm run dev       # Vite dev server (использует src/main.tsx + index.html)
npm run build     # tsc type-check + Vite IIFE → dist/widget.iife.js
npm run preview   # превью продакшен-сборки

Виджет имеет два entry-point'а:

  • src/main.tsx — обычный Vite-app для dev-режима (рендерит <App> в #app).
  • src/widget.tsx — продакшен-entry, билдится в dist/widget.iife.js. Регистрирует window.fw(...) и монтирует виджет в Shadow DOM.

Ручная проверка

После npm run build открой test.html в браузере — он подгружает локальный dist/widget.iife.js и вызывает fw('init', {...}) с тестовыми token/partnerId/baseUrl. Заменяй значения на свои перед тестом.