@tinrun/ui

Shared interface of tin apps: theme tokens, base styles and Vue 3 components — one look for the platform and its apps.
137
Install
npm install @tinrun/ui
Latest Version:1.0.0
Vue:3
License:MIT
Last Updated:Sep 30, 2026
Links: npm
Maintainer: tinsystems

@tinrun/ui

Общий интерфейс приложений tin: цвета, шрифты, отступы, кнопки, поля, окна, раскладка на телефоне — один вид и одно поведение у всех сервисов платформы и у самого tin. Пользовательские приложения могут взять его по желанию — и выглядеть «как tin» без усилий.

Витрина со всеми компонентами во всех состояниях, в обеих темах и на телефоне — tin.bar/ui (открывается без входа) или npm run dev в этом репозитории.

Два слоя:

  1. Токены и базовые стили — CSS без фреймворка, для любого стека (React, Svelte, чистый HTML).
  2. Компоненты Vue 3 — @tinrun/ui/vue.

Подключение

npm i @tinrun/ui

Vue 3 — одна строка

import { TinButton, TinAppLayout, applyTinTheme } from '@tinrun/ui/vue'

Импорт @tinrun/ui/vue сам подключает стили: токены, базовые стили и стили компонентов (сборщик приложения — Vite и т.п. — обрабатывает CSS из пакета). Больше ничего не нужно.

Любой стек — только CSS

import '@tinrun/ui/tokens.css'   // переменные --tin-* обеих тем
import '@tinrun/ui/base.css'     // шрифт, фон, ссылки, фокус, выделение (необязательно)
<!-- без сборщика -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tinrun/ui@1/dist/tokens.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tinrun/ui@1/dist/base.css" />
<button style="background: var(--tin-color-accent); color: var(--tin-color-on-accent)">Готово</button>

Темы

Тема выбирается атрибутом data-theme="light|dark" на <html>; без него — как в системе. Внутри tin тема приходит от tin — свяжите их одной строкой:

import { connect } from '@tinrun/client'
import { applyTinTheme } from '@tinrun/ui'      // или из '@tinrun/ui/vue'

const tin = await connect({ spa: true })
applyTinTheme(tin)   // data-theme = tin.theme, и дальше — вслед за themeChanged. Возвращает «перестать следить»
tin.ready()

applyTinTheme принимает любой объект с theme и on('themeChanged', …) — зависимости от @tinrun/client нет. setTinTheme('dark' | 'light' | null, root?) — поставить тему руками (null — системная). data-theme можно поставить и на любой элемент — эта часть страницы будет в другой теме (так витрина показывает обе темы рядом).

Токены

Все значения — CSS-переменные --tin-* (из tokens.css) и те же значения в JS (import { tokens } from '@tinrun/ui').

Группа Переменные
Цвета (свои в каждой теме) --tin-color-bg, surface (тихие области: код, боковое меню), surface-raised (меню, окна, уведомления), hover, text, text-muted, border (разделители), border-strong (контуры полей, 3:1), accent, on-accent, focus, danger, on-danger, danger-soft, success, success-soft, warning, warning-soft, info, info-soft, overlay, selection, skeleton, chart
Тени (свои в каждой теме) --tin-shadow-sm, --tin-shadow-md
Отступы --tin-space-1 … --tin-space-8: 4, 8, 12, 16, 24, 32, 48, 64 px
Радиусы --tin-radius-sm 4px, md 6px (кнопки, поля), lg 10px (карточки, окна), full
Шрифт --tin-font-sans, --tin-font-mono, `--tin-font-size-xs
Размеры --tin-size-control — высота кнопок и полей: 36px, 44px на телефоне и сенсорных экранах; --tin-size-touch 44px; --tin-size-sidebar 240px; --tin-size-content 960px; --tin-size-icon 20px
Слои --tin-z-dropdown, --tin-z-sticky, --tin-z-dialog, --tin-z-toast
Движение --tin-motion-fast 120ms, --tin-motion-normal 200ms (0ms при prefers-reduced-motion), --tin-motion-easing

Широкий экран — от 768px (@media (min-width: 768px); в медиазапросах переменные не работают). Контраст всех пар текст/фон — WCAG AA в обеих темах, контуры и фокус — 3:1 (проверяется тестами).

Компоненты (@tinrun/ui/vue)

Общее для всех: только токены (обе темы), от 360 px ширины, касание ≥ 44 px на телефоне, управление с клавиатуры, видимый фокус, aria. Тексты по умолчанию — русские, все заменяются свойствами (closeText, copyText, …). Компоненты не ходят в сеть. Атрибуты и слушатели, не описанные в свойствах, уходят на основной элемент (<button>, <input>, …).

Действия

TinButton — кнопка вместо голого <button>. variant: primary (главное действие экрана) | secondary (по умолчанию) | danger | ghost; loading; disabled; type (button по умолчанию); block (на всю ширину); icon (имя TinIcon); href — ссылка в виде кнопки.

<TinButton variant="primary" :loading="saving" @click="save">Сохранить</TinButton>

TinIconButton — квадратная кнопка с иконкой. icon, label (обязателен: aria-label и подсказка), variant (ghost по умолчанию), loading, disabled.

<TinIconButton icon="copy" label="Скопировать" @click="copy" />

TinMenu — выпадающее меню. items: { id, label, icon?, danger?, disabled? }[], label, align (start|end), icon кнопки; событие select(id). Своя кнопка — слот trigger (v-bind="props"). Клавиатура: Enter/Space/↓ открывает на первом пункте, ↑ — на последнем, ↑ ↓ Home End, буква — к пункту, Esc закрывает и возвращает фокус, Tab закрывает.

<TinMenu :items="[{ id: 'delete', label: 'Удалить', icon: 'trash', danger: true }]" label="Действия" @select="onAction" />

TinIcon — иконка открытого набора Lucide по имени. name, size (CSS-длина), label (если иконка значит что-то сама по себе). Встроенные имена: activity, arrow-down|left|right|up, bell, bot, box, calendar, chart, check, chevron-down|left|right|up, circle-alert, circle-check, circle-help, circle-x, clock, code, copy, cpu, credit-card, database, download, external-link, eye, eye-off, file, file-text, filter, folder, globe, grid, history, home, image, inbox, info, key, layers, link, list, lock, log-in, log-out, menu, message, minus, moon, more-horizontal, more-vertical, palette, paperclip, pause, pencil, play, plug, plus, refresh, rocket, rotate-ccw, search, send, server, settings, shield, sparkles, square, star, sun, terminal, trash, triangle-alert, upload, user, wallet, wrench, x. Другие иконки Lucide:

import { Bike } from 'lucide'
registerTinIcons({ bike: Bike })   // <TinIcon name="bike" />

Ввод

TinField — подпись, подсказка и ошибка поля, связанные для экранных дикторов. label, hint, error (показывается вместо подсказки и делает поле aria-invalid), required. Для своего поля — слот отдаёт id и describedBy.

<TinField label="Название" hint="Видно только вам" :error="nameError" required>
  <TinInput v-model="name" />
</TinField>

TinInput — поле в одну строку. v-model, type (text, search, email, url, tel, password, number), invalid, icon (слева).

TinTextarea — многострочное поле, растёт по высоте. v-model, rows (3), maxRows (12), invalid; метод focus().

TinSelect — выбор из списка (нативный <select>: системный выбор на телефоне). v-model, options: { value, label, disabled? }[], placeholder, invalid.

TinCheckbox — флажок с подписью (слот). v-model, disabled.

TinSwitch — переключатель для настройки, которая действует сразу (role="switch"). v-model, disabled, подпись — слот.

Окна

TinDialog — модальное окно. v-model:open, title, description, size (sm|md|lg), dismissible (Esc, фон и ✕ закрывают; false — только кнопки окна), closeText; слоты: содержимое и footer; событие close. Фокус — на элемент с autofocus, иначе на первое поле; Tab ходит по кругу внутри; страница под окном не прокручивается; после закрытия фокус возвращается.

TinConfirm / confirm() — подтверждение одной строкой кода:

if (await confirm({ title: 'Удалить проект?', text: 'Его нельзя будет вернуть.', confirmText: 'Удалить', danger: true })) {
  await remove()
}
if (await confirm('Выйти без сохранения?')) close()

confirm() сама показывает окно и убирает его. В шаблоне: <TinConfirm v-model:open="asking" title="…" danger @confirm="…" @cancel="…" />. Свойства: title, text, confirmText, cancelText, danger (красная кнопка, фокус на «Отмена»).

TinToast / toast() — короткие уведомления внизу экрана.

toast('Скопировано')
toast({ message: 'Не удалось сохранить', tone: 'danger', action: { label: 'Повторить', onClick: save } })

tone: info | success | warning | danger; timeout (мс, 5000; danger — 8000; 0 — пока не закроют); возвращает id для dismissToast(id). Место для уведомлений <TinToast /> необязательно — toast() создаст его сам. Уведомления читаются экранными дикторами (ошибки — сразу), таймер стоит, пока на уведомлении указатель или фокус. Пока открыт модальный TinDialog, уведомления видны под его фоном.

Раскладка

TinAppLayout — рамка экрана приложения: верхняя строка (название и действия), меню разделов и содержимое. Широкий экран (≥ 768 px) — меню слева; телефон — вкладки снизу (mobile-nav="bottom", по умолчанию) или сверху ("top"). items: { id, label, icon?, href?, badge? }[], active, title, navLabel; событие navigate(id, event) (для пунктов с href — event.preventDefault(), чтобы перейти роутером приложения). Слоты: содержимое, brand (вместо названия), actions (справа в строке), nav-footer (под меню, только на широком экране).

<TinAppLayout title="Студия" :items="sections" :active="route.name" @navigate="(id) => router.push({ name: id })">
  <template #actions><TinIconButton icon="settings" label="Настройки" /></template>
  <RouterView />
</TinAppLayout>

TinTabs — вкладки. v-model (id вкладки), tabs: { id, label, icon?, badge?, disabled? }[], label; содержимое выбранной — в слоте ({ active }). Клавиатура: ← → Home End (выбирают сразу), Tab — в панель.

TinCard — блок с рамкой. title, as (section|article|div|li), padding (none|sm|md); слоты: содержимое, actions, footer.

TinList — список строк с разделителями. items, itemKey (поле или функция), selectable (строки — кнопки, событие select(item)), label; слоты item ({ item, index }) и empty.

Состояния

TinSpinner — загрузка. size, label (тогда это role="status" для дикторов).

TinSkeleton — заготовка загружающегося содержимого. width, height, lines, circle.

TinEmpty — «пусто». icon, title, text; кнопки — в слоте.

TinError — ошибка с «Повторить». title («Что-то пошло не так»), text, retryText, retrying; событие retry (без слушателя кнопки нет); role="alert".

TinBadge — метка. tone: neutral | accent | success | warning | danger | info.

TinStatus — состояние точкой и словом. status: running (Работает), starting, waking, stopping, stopped, sleeping, crashed (Упал), error, failed, building, deploying, live, pending, paid, expired, cancelled, blocked; незнакомый показывается как есть. label, tone — заменить слово и цвет.

Данные

TinCopyField — значение с «Скопировать». value, label, secret (скрыто до «Показать»; копируется настоящее значение), тексты copyText, copiedText, failedText, showText, hideText; событие copy.

TinCode — моноширинный блок с копированием. code, language (подпись), copyable (true), wrap. Код показывается только как текст.

TinMeter — «использовано / лимит». label, value, max, format, warnAt (0.8 — с этой доли жёлтый; на лимите — красный), ofText. role="meter".

TinBarChart — столбцы на SVG без библиотек. data: { label, value }[], label, height (160), format, hint, emptyText. Подсказка со значением — при наведении и с клавиатуры: Tab на график, ← → Home End, Esc; значение объявляется дикторам, данные для них — ещё и таблицей.

TinTime — «5 мин. назад» (обновляется сама) или дата, в поясе пользователя; полная дата — в подсказке. value (Date, ISO-строка, мс), mode (relative|absolute), timeZone, locale.

TinNumber — «1 234 567», compact — «1,2 млн» (точное — в подсказке). maximumFractionDigits, locale. Без Vue — formatNumber, formatRelativeTime, formatDateTime из @tinrun/ui.

Недоверенный текст

TinMarkdown — markdown ответов ИИ и любых данных пользователей. source, openLink (в приложении tin — (url) => tin.openLink(url); без него ссылка откроется в новой вкладке).

<TinMarkdown :source="answer" :open-link="(url) => tin.openLink(url)" />

Как защищено (renderMarkdown из @tinrun/ui — то же без Vue):

  • HTML внутри markdown показывается текстом, а итог проходит через DOMPurify с коротким списком тегов: нет <script>, обработчиков (onerror, …), style, class, iframe, форм, SVG;
  • ссылки — только абсолютные http(s) (остальные теряют ссылку, текст остаётся), rel="noopener noreferrer"; клик — через openLink;
  • картинки с внешних адресов не загружаются (вместо них — подпись): внешняя картинка сообщила бы постороннему серверу, кто читает текст. Остаются только встроенные data:image/png|jpeg|gif|webp.

Правила для своих экранов

  • Цвета и размеры — только var(--tin-…); обе темы; вёрстка от 360 px.
  • Не делайте свой вход и не показывайте ключи целиком: TinCopyField secret.
  • Данные пользователей и ответы ИИ — только текстом или через TinMarkdown, никогда v-html с сырым текстом.
  • Внешние ссылки — через tin.openLink.

Витрина — приложение tin.bar/ui

showcase/ — сайт на Vite: все компоненты во всех состояниях, переключатель темы, «обе темы рядом», «ширина телефона 390 px». Внутри tin она подключает @tinrun/client с адреса tin (VITE_TIN_URL, по умолчанию https://tin.bar), вызывает tin.ready() и следует теме tin (applyTinTheme); вне tin работает сама.

npm run dev                                 # http://localhost:5013 (PORT=… — другой порт)
VITE_TIN_URL=https://tin.bar npm run build:showcase   # → showcase/dist/, статический сайт

Регистрация как нашего приложения с гостевым доступом (открывается по tin.bar/ui без входа) — в tinAPI:

npm run apps:register -- --id ui --title "tin UI" --url https://ui.tin.bar/ --guest-access

Хостинг — любой статический (showcase/dist/), с заголовком Content-Security-Policy: frame-ancestors https://tin.bar (стандарт приложения, п. 7). Локально: tinAPI с APPS_ALLOW_LOCALHOST=true, --url http://localhost:5013/, tinWeb с VITE_TIN_URL витрины на адрес этого tinWeb.

Разработка

npm ci
npm run dev          # витрина
npm run lint && npm run type-check && npm test && npm run build

Правила изменений — CLAUDE.md; история — CHANGELOG.md.

Версии и публикация

Как у @tinrun/client / @tinrun/server: 1.x — только совместимо (новые компоненты, свойства, токены); переименование и удаление — 2.0, 1.x поддерживается 12 месяцев. Публикация — npm publish --provenance из CI по тегу v<версия> (.github/workflows/publish.yml, секрет NPM_TOKEN).

Лицензия

MIT. Иконки — Lucide (ISC).

Related Packages

@apform-ui/core

Apform UI:Element Plus 底座包装在本库栈下;el-* 算可用能力,视觉以本库 Design T...

8,379 0
blessing-ui

Blessing UI — Themed Flat Interface. Flat by design, raised with Vue, blessed fo...

610 0
seeroad-ui

seeroad project ui library

3,733
ui
smart-ui

High Quality Mobile UI Componets by Vue.js 2.

1,883 11
ui

Version History

Version Released Vue License
1.0.0 ^3.5.0 MIT