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

@ykurilov/tandem-ui

v0.7.0

Published

React-компоненты и токены дизайн-системы Тандем. React 19, Tailwind CSS 4.

Readme

Tandem UI

React-компоненты и токены дизайн-системы Тандем. React 19, Tailwind CSS 4.

Как подключить

Пакет лежит на складе GitHub Packages. В .npmrc проекта:

@ykurilov:registry=https://npm.pkg.github.com

Токен с правом read:packages — в ~/.npmrc (строка //npm.pkg.github.com/:_authToken=…). Потом:

pnpm add @tandem/ui@npm:@ykurilov/tandem-ui@^0.7.0

В главном CSS приложения:

@import "tailwindcss";
@import "@tandem/ui/styles.css";
/* Tailwind должен видеть классы компонентов, иначе часть стилей не попадёт в сборку. */
@source "../../node_modules/@tandem/ui/dist";

Путь в @source считается от этого CSS-файла до node_modules.

Надписи на другом языке

Компоненты сами пишут и озвучивают около полусотни надписей: «Отправить», «Скопировать код», «Закрыть», «Загружается…». По умолчанию они русские. Другой язык ставится один раз у корня приложения:

"use client"
import { TandemLabelsProvider, englishTandemLabels } from "@tandem/ui"

export function Providers({ children }: { children: React.ReactNode }) {
  return <TandemLabelsProvider labels={englishTandemLabels}>{children}</TandemLabelsProvider>
}
  • Словарь — объект типа TandemLabels, по группе на компонент: appShell, composer, dialog, markdown, userMessage. Готовые: russianTandemLabels и englishTandemLabels.
  • Свой язык собирайте по типу: const german: TandemLabels = { … } — компилятор подскажет пропущенное. Надписи с числом или именем — функции: removeAttachment: name => `Remove ${name}`.
  • Можно передать часть словаря: labels={{ composer: { send: "Go" } }}. Остальное возьмётся у внешнего провайдера, в конце — русское.
  • У каждого из этих компонентов есть свойство labels со своей группой, оно главнее провайдера. У Composer ещё главнее отдельные placeholder, aria-label и stopLabel.
  • Провайдеру нужен браузерный React, поэтому в Next.js он живёт в файле с "use client", как в примере.

Каркас приложения: AppShell

Боковое меню и рабочая область рядом. Умеет:

  • знак и название, пункты со значками и счётчиками, группы с подписями;
  • активный пункт по текущему адресу;
  • человека внизу и меню профиля;
  • сворачивание и перетаскивание ширины, с памятью между визитами и без мигания при загрузке;
  • на телефоне — выезжающую панель с кнопкой открытия: фокус не выходит из панели, Escape закрывает;
  • необязательный поиск по разделам.

Роутер оболочке не нужен. Ссылки рисует компонент из linkComponent, по умолчанию — обычный <a>.

Next.js 16

Оболочке нужен браузер, поэтому она живёт в своём файле с "use client". Серверный layout передаёт в неё пользователя и счётчики.

// app/(cabinet)/cabinet-shell.tsx
"use client"

import Link from "next/link"
import { usePathname } from "next/navigation"
import { Inbox, LogOut, Settings } from "lucide-react"
import { AppShell, TandemMark } from "@tandem/ui"

export function CabinetShell({ user, unanswered, children }: {
  user: { name: string; email: string }
  unanswered: number
  children: React.ReactNode
}) {
  return (
    <AppShell
      linkComponent={Link}
      currentPath={usePathname()}
      brand={{ logo: <TandemMark />, name: "Живые лидмагниты", href: "/" }}
      navigation={[
        { href: "/inbox", label: "Входящие", icon: Inbox, badge: unanswered, badgeLabel: `${unanswered} без ответа` },
        { href: "/settings", label: "Настройки", icon: Settings }
      ]}
      user={user}
      userMenu={[{ href: "/api/auth/logout", label: "Выйти", icon: LogOut }]}
    >
      {children}
    </AppShell>
  )
}
// app/(cabinet)/layout.tsx — серверный
export default async function CabinetLayout({ children }: { children: React.ReactNode }) {
  const user = await requireUser()
  const unanswered = await countUnansweredThreads(user.id)
  return <CabinetShell user={user} unanswered={unanswered}>{children}</CabinetShell>
}

Чтобы меню не мигало при загрузке, в <head> корневого app/layout.tsx ставится AppShellScript. Это маленький скрипт: до первой отрисовки он достаёт из localStorage свёрнутость и ширину и пишет их на <html>. Компонент без хуков, корневой layout остаётся серверным.

// app/layout.tsx — корневой, серверный
import { AppShellScript } from "@tandem/ui"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ru" suppressHydrationWarning>
      <head>
        <AppShellScript />
      </head>
      <body>{children}</body>
    </html>
  )
}
  • Скрипт — только в корневом layout. Во вложенном layout React создал бы его заново при переходе на клиенте, а такой скрипт не выполняется, и React предупреждает об этом в консоли.
  • Если у AppShell заданы свои storageKey, defaultCollapsed, defaultWidth, minWidth или maxWidth, те же значения передаются в AppShellScript. Свёрнутостью или шириной управляет приложение (свойства collapsed, width) — rememberCollapsed={false} или rememberWidth={false}. storageKey={null} у оболочки — скрипт не нужен.
  • suppressHydrationWarning у <html> обязателен: скрипт меняет атрибуты <html> до гидратации, и без флага React предупредит, что они расходятся с серверными. Флаг действует только на сам <html>.
  • Строгая политика безопасности (CSP): <AppShellScript nonce={nonce} />.

Без серверной отрисовки (обычное приложение на Vite) скрипт не нужен: оболочка читает сохранённое до первой отрисовки сама.

«Выйти» — обычная ссылка, не кнопка с JavaScript. Адреса /api/… и внешние ссылки оболочка всегда рисует простым <a>, мимо next/link: тот подгружает адреса заранее, и подгрузка выхода разлогинила бы человека. Для других адресов то же включает reloadDocument: true.

Vite и React Router

import { Link, useLocation } from "react-router"
import { AppShell } from "@tandem/ui"

function RouterLink({ href, ...props }: React.ComponentProps<"a"> & { href: string }) {
  return <Link to={href} {...props} />
}

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <AppShell linkComponent={RouterLink} currentPath={useLocation().pathname} brand={…} navigation={…}>
      {children}
    </AppShell>
  )
}

Главные свойства

| Свойство | Что делает | | --- | --- | | brand | { logo, name, caption?, href? }. В свёрнутом меню остаётся знак. | | navigation | Пункты { href, label, icon?, badge?, badgeLabel?, active? } или группы { label, items }. Счётчик больше 99 показан как «99+». badgeLabel — что услышит человек с экранным диктором, например «120 без ответа». | | currentPath | Текущий адрес. Активен пункт с самым длинным подходящим href. Флаг active у пункта главнее. | | user, userMenu | Человек { name, email?, avatarUrl? } и пункты его меню: ссылка { label, href }, действие { label, onSelect }, { type: "separator" }, { type: "label", label }. | | linkComponent | Компонент ссылки роутера. Получает href, className, onClick, aria-current. | | header | Верхняя полоса над содержимым. Без неё полоса есть только на телефоне: кнопка меню и знак. | | search | { items: [{ label, href, hint?, keywords? }], placeholder? }. | | sidebarFooter | Блок над пользователем. | | storageKey | Ключ в localStorage, по умолчанию "tandem-sidebar". null — ничего не запоминать. | | collapsed, width | Управляемые свёрнутость и ширина, если приложение хранит их само (например, в cookie). Пары: onCollapsedChange, onWidthChange. | | defaultCollapsed, defaultWidth, minWidth, maxWidth | Начальные значения и пределы: 264, 220 и 380 px. | | mainId, mainClassName | id и классы рабочей области. На неё ведёт ссылка «Перейти к содержимому». | | labels | Подписи для другого языка. Главнее общего TandemLabelsProvider (см. «Надписи на другом языке»). |

Внутри оболочки useAppShell() даёт collapsed, mobileOpen, openMobileMenu, closeMobileMenu — например, чтобы закрыть панель из своего кода.