hallo-kit 0.3.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.
Files changed (128) hide show
  1. package/README.md +168 -0
  2. package/bin/cli.mjs +136 -0
  3. package/fonts/Manrope/Manrope-VariableFont_wght.woff2 +0 -0
  4. package/fonts/Manrope/OFL.txt +93 -0
  5. package/fonts/index.ts +30 -0
  6. package/package.json +77 -0
  7. package/src/config.ts +11 -0
  8. package/src/hooks/index.ts +14 -0
  9. package/src/hooks/useAutoHideOnScroll.ts +26 -0
  10. package/src/hooks/useCountdown.ts +44 -0
  11. package/src/hooks/useDevice.ts +63 -0
  12. package/src/hooks/useIsTelegramMiniApp.ts +17 -0
  13. package/src/hooks/useIsTouch.ts +23 -0
  14. package/src/hooks/useKeyboardOpen.ts +35 -0
  15. package/src/hooks/useMediaQuery.ts +24 -0
  16. package/src/hooks/usePlatform.ts +102 -0
  17. package/src/hooks/useQueryParams.tsx +70 -0
  18. package/src/hooks/useRevealOnce.ts +21 -0
  19. package/src/hooks/useScrolledPast.ts +19 -0
  20. package/src/hooks/useSettleOnce.ts +26 -0
  21. package/src/hooks/useTelegram.tsx +57 -0
  22. package/src/i18n/index.tsx +92 -0
  23. package/src/index.ts +35 -0
  24. package/src/lib/device.ts +28 -0
  25. package/src/lib/platform.ts +19 -0
  26. package/src/lib/utils.ts +36 -0
  27. package/src/logo/LogoFull.tsx +59 -0
  28. package/src/logo/LogoMark.tsx +31 -0
  29. package/src/logo/index.ts +2 -0
  30. package/src/next.ts +13 -0
  31. package/src/server.ts +4 -0
  32. package/src/theme-color-sync.tsx +43 -0
  33. package/src/theme-provider.tsx +31 -0
  34. package/src/types/css.d.ts +6 -0
  35. package/src/types/globals.d.ts +47 -0
  36. package/src/ui/Icon/Icon.module.css +25 -0
  37. package/src/ui/Icon/Icon.tsx +45 -0
  38. package/src/ui/Icon/Icon.types.ts +60 -0
  39. package/src/ui/Icon/data/brand.data.tsx +75 -0
  40. package/src/ui/Icon/data/index.ts +2 -0
  41. package/src/ui/Icon/data/ui.data.tsx +780 -0
  42. package/src/ui/Icon/index.ts +3 -0
  43. package/src/ui/badge/badge.tsx +185 -0
  44. package/src/ui/badge/index.ts +1 -0
  45. package/src/ui/breadcrumbs/breadcrumbs.tsx +63 -0
  46. package/src/ui/breadcrumbs/index.ts +1 -0
  47. package/src/ui/burger-icon/burger-icon.tsx +22 -0
  48. package/src/ui/burger-icon/index.ts +1 -0
  49. package/src/ui/button/button.tsx +179 -0
  50. package/src/ui/button/index.ts +1 -0
  51. package/src/ui/card/card.tsx +100 -0
  52. package/src/ui/card/index.ts +1 -0
  53. package/src/ui/confirm/confirm-dialog-host.tsx +89 -0
  54. package/src/ui/confirm/index.ts +1 -0
  55. package/src/ui/count-up/count-up.tsx +96 -0
  56. package/src/ui/count-up/index.ts +1 -0
  57. package/src/ui/data-list/data-list.tsx +331 -0
  58. package/src/ui/data-list/index.ts +1 -0
  59. package/src/ui/drawer/drawer.tsx +166 -0
  60. package/src/ui/drawer/index.ts +1 -0
  61. package/src/ui/flag/flag.data.ts +240 -0
  62. package/src/ui/flag/flag.tsx +57 -0
  63. package/src/ui/flag/index.ts +1 -0
  64. package/src/ui/image/image.tsx +67 -0
  65. package/src/ui/image/index.ts +1 -0
  66. package/src/ui/input/amount-input.tsx +55 -0
  67. package/src/ui/input/index.ts +2 -0
  68. package/src/ui/input/input.tsx +391 -0
  69. package/src/ui/label/index.ts +1 -0
  70. package/src/ui/label/label.tsx +34 -0
  71. package/src/ui/method-card/index.ts +1 -0
  72. package/src/ui/method-card/method-card.tsx +101 -0
  73. package/src/ui/modal/index.ts +1 -0
  74. package/src/ui/modal/modal.tsx +128 -0
  75. package/src/ui/otp-input/index.ts +1 -0
  76. package/src/ui/otp-input/otp-input.tsx +78 -0
  77. package/src/ui/pagination/index.ts +1 -0
  78. package/src/ui/pagination/pagination.tsx +138 -0
  79. package/src/ui/radio-group/index.ts +1 -0
  80. package/src/ui/radio-group/radio-group.tsx +81 -0
  81. package/src/ui/select/index.ts +1 -0
  82. package/src/ui/select/select.tsx +215 -0
  83. package/src/ui/separator/index.ts +1 -0
  84. package/src/ui/separator/separator.tsx +54 -0
  85. package/src/ui/skeleton/index.ts +1 -0
  86. package/src/ui/skeleton/skeleton.tsx +44 -0
  87. package/src/ui/spinner/index.ts +1 -0
  88. package/src/ui/spinner/spinner.tsx +33 -0
  89. package/src/ui/switch/index.ts +1 -0
  90. package/src/ui/switch/switch.tsx +48 -0
  91. package/src/ui/tabs/index.ts +1 -0
  92. package/src/ui/tabs/tabs.tsx +190 -0
  93. package/src/ui/textarea/index.ts +1 -0
  94. package/src/ui/textarea/textarea.tsx +151 -0
  95. package/src/ui/theme-switch/index.ts +1 -0
  96. package/src/ui/theme-switch/theme-switch.tsx +161 -0
  97. package/src/ui/toast/index.ts +1 -0
  98. package/src/ui/toast/toast.tsx +159 -0
  99. package/src/utils/api/stringFormat.ts +5 -0
  100. package/src/utils/clearAllCookies.ts +7 -0
  101. package/src/utils/clsx.ts +18 -0
  102. package/src/utils/confirm.ts +48 -0
  103. package/src/utils/copy-to-clipboard.ts +12 -0
  104. package/src/utils/date/convertTimestamp.ts +44 -0
  105. package/src/utils/date/formatCountdown.ts +9 -0
  106. package/src/utils/date/formatDayLabel.ts +50 -0
  107. package/src/utils/date/formatRelativeDateTime.ts +34 -0
  108. package/src/utils/date/locales.ts +9 -0
  109. package/src/utils/form/validationSchema.ts +46 -0
  110. package/src/utils/getByKeyString.ts +15 -0
  111. package/src/utils/getCssVar.ts +2 -0
  112. package/src/utils/getDeepType.ts +3 -0
  113. package/src/utils/getQueryParams.ts +21 -0
  114. package/src/utils/inAppBrowser.ts +151 -0
  115. package/src/utils/index.ts +28 -0
  116. package/src/utils/isRTL.ts +29 -0
  117. package/src/utils/notify.ts +46 -0
  118. package/src/utils/openExternal.ts +27 -0
  119. package/src/utils/system/getMillisecondsPeriod.ts +31 -0
  120. package/src/utils/validation/authValidators.ts +23 -0
  121. package/src/utils/validation/refCode.ts +30 -0
  122. package/styles/base.css +97 -0
  123. package/styles/brand.css +36 -0
  124. package/styles/index.css +19 -0
  125. package/styles/motion.css +207 -0
  126. package/styles/semantic.css +151 -0
  127. package/styles/theme.css +143 -0
  128. package/styles/utilities.css +289 -0
@@ -0,0 +1,28 @@
1
+ // Утилиты кита: классы, буфер обмена, даты, платформа, валидация, порт уведомлений.
2
+
3
+ export * from './api/stringFormat'
4
+ export * from './clearAllCookies'
5
+ export * from './clsx'
6
+ export * from './confirm'
7
+ export * from './copy-to-clipboard'
8
+ export * from './date/convertTimestamp'
9
+ export * from './date/formatCountdown'
10
+ export * from './date/formatDayLabel'
11
+ export * from './date/formatRelativeDateTime'
12
+ export * from './date/locales'
13
+ export * from './form/validationSchema'
14
+ export * from './getByKeyString'
15
+ export * from './getCssVar'
16
+ export * from './getDeepType'
17
+ export * from './getQueryParams'
18
+ export * from './inAppBrowser'
19
+ export * from './isRTL'
20
+ export * from './notify'
21
+ export * from './openExternal'
22
+ export * from './system/getMillisecondsPeriod'
23
+ export * from './validation/authValidators'
24
+ export * from './validation/refCode'
25
+
26
+ // cn() и определение платформы — из lib, но публикуются здесь: это тоже утилиты.
27
+ export * from '../lib/utils'
28
+ export * from '../lib/platform'
@@ -0,0 +1,29 @@
1
+ const rtlLocales = [
2
+ 'ae' /* Avestan */,
3
+ 'ar' /* 'العربية', Arabic */,
4
+ 'arc' /* Aramaic */,
5
+ 'bcc' /* 'بلوچی مکرانی', Southern Balochi */,
6
+ 'bqi' /* 'بختياري', Bakthiari */,
7
+ 'ckb' /* 'Soranî / کوردی', Sorani */,
8
+ 'dv' /* Dhivehi */,
9
+ 'fa' /* 'فارسی', Persian */,
10
+ 'glk' /* 'گیلکی', Gilaki */,
11
+ 'he' /* 'עברית', Hebrew */,
12
+ 'ku' /* 'Kurdî / كوردی', Kurdish */,
13
+ 'mzn' /* 'مازِرونی', Mazanderani */,
14
+ 'nqo' /* N'Ko */,
15
+ 'pnb' /* 'پنجابی', Western Punjabi */,
16
+ 'ps' /* 'پښتو', Pashto, */,
17
+ 'sd' /* 'سنڌي', Sindhi */,
18
+ 'ug' /* 'Uyghurche / ئۇيغۇرچە', Uyghur */,
19
+ 'ur' /* 'اردو', Urdu */,
20
+ 'yi' /* 'ייִדיש', Yiddish */,
21
+ ]
22
+
23
+ export const isRTL = (locale: string) => {
24
+ return rtlLocales.indexOf(locale.split('-')[0]) !== -1
25
+ }
26
+
27
+ export const getCurrentDir = (locale: string) => {
28
+ return isRTL(locale) ? 'rtl' : 'ltr'
29
+ }
@@ -0,0 +1,46 @@
1
+ // UI-агностичная абстракция тостов/уведомлений.
2
+ //
3
+ // Инфраструктурный код (например, axios-interceptor в clientApi) дёргает `notify(...)`,
4
+ // НЕ зная, какой UI-кит показывает тост. Слой UI один раз вызывает
5
+ // `registerNotifyHandler(...)` (обычно в провайдере) и подключает реальный тост.
6
+ // Пока обработчик не зарегистрирован — фолбэк в консоль, чтобы ничего не падало.
7
+
8
+ export type NotifyVariant = 'error' | 'success' | 'info' | 'warning'
9
+
10
+ export interface NotifyOptions {
11
+ title: string
12
+ description?: string
13
+ variant?: NotifyVariant
14
+ /** Время автозакрытия в мс. Интерпретация — на стороне UI-обработчика. */
15
+ timeout?: number
16
+ }
17
+
18
+ export type NotifyHandler = (options: NotifyOptions) => void
19
+
20
+ let handler: NotifyHandler | null = null
21
+
22
+ /**
23
+ * Подключить реальный показ тостов. Возвращает функцию-отписку — удобно звать
24
+ * из `useEffect` в провайдере.
25
+ */
26
+ export const registerNotifyHandler = (next: NotifyHandler): (() => void) => {
27
+ handler = next
28
+ return () => {
29
+ if (handler === next) handler = null
30
+ }
31
+ }
32
+
33
+ export const notify = (options: NotifyOptions): void => {
34
+ if (handler) {
35
+ handler(options)
36
+ return
37
+ }
38
+
39
+ // Фолбэк до подключения UI-обработчика.
40
+ const tag = `[notify:${options.variant ?? 'info'}]`
41
+ if (options.variant === 'error') {
42
+ console.error(tag, options.title, options.description ?? '')
43
+ } else {
44
+ console.warn(tag, options.title, options.description ?? '')
45
+ }
46
+ }
@@ -0,0 +1,27 @@
1
+ 'use client'
2
+
3
+ import { getTelegram, getTelegramWebApp } from '../hooks/useTelegram'
4
+
5
+ // Открытие внешней ссылки с учётом платформы (паттерн nerp Setup.tsx):
6
+ // • Telegram MiniApp + http(s) — tg.openLink(): системный браузер поверх мини-аппа
7
+ // (window.open из WebView Telegram либо глушится, либо уводит юзера из аппа);
8
+ // • Telegram MiniApp + кастомная схема (happ://…) — location.href: openLink не умеет
9
+ // не-http схемы, а прямой переход отдаёт диплинк ОС;
10
+ // • обычный браузер — window.open в новой вкладке.
11
+ // Использовать для ВСЕХ наружных переходов: ссылка оплаты, скачивание Happ, диплинки.
12
+ export function openExternal(url?: string | null) {
13
+ if (!url) return
14
+
15
+ const isHttp = /^https?:\/\//i.test(url)
16
+ const webApp = getTelegramWebApp()
17
+
18
+ if (webApp?.openLink && isHttp) {
19
+ webApp.openLink(url)
20
+ return
21
+ }
22
+ if (getTelegram() && !isHttp) {
23
+ window.location.href = url
24
+ return
25
+ }
26
+ window.open(url, '_blank', 'noopener,noreferrer')
27
+ }
@@ -0,0 +1,31 @@
1
+ type PreProps = {
2
+ sec?: never
3
+ min?: never
4
+ hour?: never
5
+ day?: never
6
+ }
7
+
8
+ type SecProps = Omit<PreProps, 'sec'> & {
9
+ sec: number
10
+ }
11
+
12
+ type MinProps = Omit<PreProps, 'min'> & {
13
+ min: number
14
+ }
15
+
16
+ type HourProps = Omit<PreProps, 'hour'> & {
17
+ hour: number
18
+ }
19
+
20
+ type DayProps = Omit<PreProps, 'day'> & {
21
+ day: number
22
+ }
23
+
24
+ type Props = SecProps | MinProps | HourProps | DayProps
25
+
26
+ export const getMillisecondsPeriod = ({ sec, min, hour, day }: Props) => {
27
+ if (sec) return 1000 * sec
28
+ if (min) return 1000 * 60 * min
29
+ if (hour) return 1000 * 60 * 60 * hour
30
+ if (day) return 1000 * 60 * 60 * 24 * day
31
+ }
@@ -0,0 +1,23 @@
1
+ import isEmail from 'validator/lib/isEmail'
2
+ import isUUID from 'validator/lib/isUUID'
3
+
4
+ /**
5
+ * Display-нормализация email: `Trim → NFC → toLowerCase`. Это форма «как ввёл
6
+ * пользователь», которая идёт в чек/письмо. Никакой gmail-специфики (точки,
7
+ * `+suffix`) — это ответственность бэка (`EmailCanonicalizer`). Фронт не должен
8
+ * схлопывать вариации сам: на других провайдерах (yandex и т.п.) точки в local-part
9
+ * — это РАЗНЫЕ ящики, и фронт не знает per-domain правил.
10
+ */
11
+ export const normalizeAuthEmail = (value: string) =>
12
+ value.trim().normalize('NFC').toLowerCase()
13
+
14
+ /** UUID-идентификатор: только trim. Регистр сохраняем (UUID-парсинг case-insensitive). */
15
+ export const normalizeRemnaIdentifier = (value: string) => value.trim()
16
+
17
+ export const isValidAuthEmail = (value: string) =>
18
+ isEmail(normalizeAuthEmail(value), {
19
+ allow_utf8_local_part: false,
20
+ require_tld: true,
21
+ })
22
+
23
+ export const isValidRemnaIdentifier = (value: string) => isUUID(normalizeRemnaIdentifier(value))
@@ -0,0 +1,30 @@
1
+ // Реф-код из ссылки (?ref=). Правила обязаны совпадать с серверными — иначе фронт
2
+ // отправит код, который сервер молча отбросит. Лимит длины взят из схемы хранения.
3
+ const MAX_LENGTH = 25
4
+ const ALLOWED_PATTERN = /^[A-Za-z0-9_-]+$/
5
+
6
+ // Служебные deep-link команды бота (?start=): рефом не являются, бот обрабатывает их сам.
7
+ const BOT_COMMAND_PREFIXES = ['link_', 'pay_stars_', 'support_']
8
+ const BOT_COMMANDS = ['pay', 'support']
9
+
10
+ const isBotStartCommand = (value: string) =>
11
+ BOT_COMMAND_PREFIXES.some(prefix => value.startsWith(prefix)) ||
12
+ BOT_COMMANDS.includes(value)
13
+
14
+ /** Нормализует `?ref=` → код или `null`, если бэк такое значение всё равно не примет. */
15
+ export const normalizeRefCode = (value: string | null | undefined): string | null => {
16
+ if (!value) {
17
+ return null
18
+ }
19
+
20
+ const trimmed = value.trim()
21
+ if (!trimmed || trimmed.length > MAX_LENGTH) {
22
+ return null
23
+ }
24
+
25
+ if (!ALLOWED_PATTERN.test(trimmed) || isBotStartCommand(trimmed)) {
26
+ return null
27
+ }
28
+
29
+ return trimmed
30
+ }
@@ -0,0 +1,97 @@
1
+ /* Базовые стили: голые теги выглядят опрятно без классов.
2
+ Источник: hallo/client — src/app/globals.css. Файл раздаётся пакетом @hallo/kit;
3
+ правки вносить ЗДЕСЬ, кабинет и сайты получают их обновлением пакета. */
4
+
5
+ /* ============================================================
6
+ Базовые стили: голые теги выглядят опрятно без классов.
7
+ Это лишь дефолты — в разметке можно переопределять утилитами.
8
+ ============================================================ */
9
+ @layer base {
10
+ /* Кольцо фокуса на кнопках, табах, радио-карточках и селектах не показываем: по решению
11
+ владельца продукта оно мешало — особенно на модалках, где Radix переводил фокус на
12
+ первый элемент при открытии и подсвечивал таб «5 устройств» до действий пользователя.
13
+
14
+ Селектор намеренно ТОЛЬКО :focus-visible. Текстовые поля показывают активность тем же
15
+ кольцом, но вешают его иначе: Input — на обёртку через focus-within, OTP-ячейки — через
16
+ обычный focus. Их это правило не задевает, иначе логин и ввод кода печатались бы вслепую.
17
+
18
+ Гасим ring-переменные Tailwind, а не box-shadow целиком: собственные тени карточек и
19
+ модалок лежат в --tw-shadow и обязаны пережить фокус.
20
+
21
+ !important обязателен: сами кольца приходят утилитами Tailwind из слоя utilities, а он
22
+ идёт ПОСЛЕ base и без important перебил бы это правило. Для important-объявлений порядок
23
+ слоёв инвертирован, поэтому base с !important выигрывает (тот же приём, что ниже у
24
+ компенсации скроллбара).
25
+
26
+ Цена решения: при ходьбе по странице с клавиатуры не видно, где сейчас фокус. Сама
27
+ фокусируемость и порядок обхода не тронуты — вернуть подсветку = удалить это правило. */
28
+ *:focus-visible {
29
+ outline: none !important;
30
+ --tw-ring-shadow: 0 0 #0000 !important;
31
+ --tw-ring-offset-shadow: 0 0 #0000 !important;
32
+ }
33
+
34
+ body {
35
+ background-color: var(--color-background);
36
+ color: var(--color-foreground);
37
+ font-family: var(--font-sans), Arial, Helvetica, sans-serif;
38
+ /* дефолтный текст — Body M */
39
+ @apply text-body-m;
40
+ }
41
+
42
+ /* Резерв места под вертикальный скроллбар — ТОЛЬКО на не-сенсорных (мышь/трекпад:
43
+ классический скроллбар «съедает» ширину) и от md-ширины (≥ 48rem = 768px). Так контент
44
+ не дёргается по горизонтали при появлении/исчезновении скролла. Ниже 768 (мобила, где
45
+ указатель грубый или скроллбар-оверлей) резерв не нужен. Действует и в Telegram
46
+ MiniApp (десктопный клиент): без резерва контент прыгает при смене страниц
47
+ со скроллом/без. */
48
+ @media (pointer: fine) and (min-width: 48rem) {
49
+ html {
50
+ scrollbar-gutter: stable;
51
+ }
52
+
53
+ /* Модалки (Radix Dialog → react-remove-scroll) при открытии компенсируют
54
+ «исчезнувший» скроллбар инжект-стилем body[data-scroll-locked]
55
+ { margin-right: <gap>px !important }. При активном scrollbar-gutter жёлоб УЖЕ
56
+ зарезервирован — компенсация лишняя и сдвигает весь контент вправо на ширину
57
+ скроллбара. Глушим её: удвоенный атрибут — специфичность выше инжекта, а
58
+ !important в @layer побеждает unlayered !important (порядок слоёв для
59
+ important-правил инвертирован). Вне gutter-условий (мобила) компенсация
60
+ нужна — там не трогаем. */
61
+ body[data-scroll-locked][data-scroll-locked] {
62
+ margin-right: 0 !important;
63
+ padding-right: 0 !important;
64
+ }
65
+ }
66
+
67
+ /* Сплэш первой загрузки: разметка И стили целиком живут в <AppSplashScreen/>
68
+ (components/app-splash-screen.tsx) — инлайн-<style> рядом с разметкой, чтобы
69
+ не зависеть от загрузки этого файла, cascade layers и HMR. */
70
+
71
+ h1,
72
+ h2,
73
+ h3,
74
+ h4,
75
+ h5,
76
+ h6 {
77
+ font-family: var(--font-display), var(--font-sans), sans-serif;
78
+ text-wrap: balance;
79
+ }
80
+
81
+ /* размер/вес/интервалы заголовков — из токенов типографики */
82
+ h1 {
83
+ @apply text-h1;
84
+ }
85
+ h2 {
86
+ @apply text-h2;
87
+ }
88
+ h3 {
89
+ @apply text-h3;
90
+ }
91
+ h4 {
92
+ @apply text-h4;
93
+ }
94
+ h5 {
95
+ @apply text-h5;
96
+ }
97
+ }
@@ -0,0 +1,36 @@
1
+ /* ПАЛИТРА БРЕНДА (raw). Единственный слой, который сайт переопределяет под себя:
2
+ продублируй эти переменные в своём globals.css ПОСЛЕ импорта кита — семантика
3
+ и все компоненты подхватят новые цвета сами.
4
+ Источник: hallo/client — src/app/globals.css. Файл раздаётся пакетом @hallo/kit;
5
+ правки вносить ЗДЕСЬ, кабинет и сайты получают их обновлением пакета. */
6
+
7
+ :root {
8
+ /* ============================================================
9
+ ПАЛИТРА БРЕНДА (raw) — фирменные цвета с исходными названиями.
10
+ Используется как источник для семантического слоя ниже.
11
+ ============================================================ */
12
+ --brand-blue: #1565ff; /* Brand Blue */
13
+ --blue-light: #4f8cff; /* вторая точка Blue Gradient */
14
+ --blue-pressed: #0f57e6; /* Blue Pressed — нажатое состояние Primary-кнопки */
15
+ --line-strong: #7f94bf; /* Line Strong — рамка вторичной кнопки (из ref-page) */
16
+ --deep-navy: #061b6a; /* Deep Navy */
17
+ --cloud-white: #fefefe; /* Cloud White */
18
+ --sky-mist: #eaf2ff; /* Sky Mist */
19
+ --ice-blue: #dce7ff; /* Ice Blue */
20
+ --connection-green: #22c55e; /* Connection Green */
21
+ --connection-green-dark: #15803d; /* контрастный success-текст на светлом soft-фоне */
22
+ --green-soft: #e8f8ee; /* Green Soft */
23
+ --error-red: #ef4444; /* Error Red */
24
+ --error-red-soft: #fef2f2; /* Error Red Soft */
25
+ --error-red-dark: #b91c1c; /* Error Red Dark */
26
+ --warning-gold: #f4b740; /* Warning Gold */
27
+ --warning-gold-dark: #8a5a00; /* контрастный warning-текст на светлом soft-фоне */
28
+ --gold-soft: #fff4d6; /* Gold Soft */
29
+ --disabled-blue: #8ea6d8; /* Disabled Blue */
30
+
31
+ --gradient-brand: linear-gradient(
32
+ 90deg,
33
+ var(--brand-blue) 0%,
34
+ var(--blue-light) 100%
35
+ );
36
+ }
@@ -0,0 +1,19 @@
1
+ /* Стили дизайн-системы Hallo. Подключение в проекте:
2
+
3
+ @import 'tailwindcss';
4
+ @import '@hallo/kit/styles.css';
5
+ @source '../../node_modules/@hallo/kit/src';
6
+
7
+ Последняя строка обязательна: без неё Tailwind не сканирует классы внутри
8
+ пакета и половина стилей компонентов не попадёт в бандл.
9
+
10
+ Свой бренд: переопредели переменные из brand.css в своём css ПОСЛЕ этого импорта. */
11
+
12
+ @import 'tw-animate-css';
13
+
14
+ @import './brand.css';
15
+ @import './semantic.css';
16
+ @import './theme.css';
17
+ @import './utilities.css';
18
+ @import './base.css';
19
+ @import './motion.css';
@@ -0,0 +1,207 @@
1
+ /* Анимации: shimmer, reveal, float, signal-pulse, modal-blur + view transitions темы.
2
+ Уважают prefers-reduced-motion.
3
+ Источник: hallo/client — src/app/globals.css. Файл раздаётся пакетом @hallo/kit;
4
+ правки вносить ЗДЕСЬ, кабинет и сайты получают их обновлением пакета. */
5
+
6
+
7
+ /* ── Skeleton shimmer ──────────────────────────────────────────────────────
8
+ Блик-полоса пробегает по заглушке слева-направо. Подключается утилитой
9
+ `animate-shimmer` на блике внутри Skeleton (см. components/ui/skeleton.tsx). */
10
+ @theme {
11
+ --animate-shimmer: shimmer 1.6s ease-in-out infinite;
12
+ }
13
+
14
+ @keyframes shimmer {
15
+ 0% {
16
+ transform: translateX(-100%);
17
+ }
18
+ 100% {
19
+ transform: translateX(100%);
20
+ }
21
+ }
22
+
23
+ /* ── Появление контента ────────────────────────────────────────────────────
24
+ Мягкое «всплывание» блоков при монтировании: opacity + подъём на 10px.
25
+ GPU-friendly (только transform/opacity). Ease «out-back-lite» (0.22,1,0.36,1) —
26
+ быстрый старт, деликатное торможение без пружины. Всё гасится reduced-motion (ниже). */
27
+ @keyframes reveal-up {
28
+ from {
29
+ opacity: 0;
30
+ transform: translateY(10px);
31
+ }
32
+ to {
33
+ opacity: 1;
34
+ transform: none;
35
+ }
36
+ }
37
+
38
+ /* Одиночное всплывание блока (напр. заголовок страницы). */
39
+ @utility reveal {
40
+ animation: reveal-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
41
+ }
42
+
43
+ /* Каскадное всплывание ПРЯМЫХ детей контейнера — каждый следующий чуть позже.
44
+ Вешать на grid/flex со списком карточек: сетка «собирается» лесенкой.
45
+ Задержка растёт до 8-го ребёнка, дальше — плато (чтобы длинные списки не тянулись). */
46
+ @utility reveal-stagger {
47
+ & > * {
48
+ animation: reveal-up 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
49
+ }
50
+ & > *:nth-child(1) {
51
+ animation-delay: 0.04s;
52
+ }
53
+ & > *:nth-child(2) {
54
+ animation-delay: 0.1s;
55
+ }
56
+ & > *:nth-child(3) {
57
+ animation-delay: 0.16s;
58
+ }
59
+ & > *:nth-child(4) {
60
+ animation-delay: 0.22s;
61
+ }
62
+ & > *:nth-child(5) {
63
+ animation-delay: 0.28s;
64
+ }
65
+ & > *:nth-child(6) {
66
+ animation-delay: 0.34s;
67
+ }
68
+ & > *:nth-child(7) {
69
+ animation-delay: 0.4s;
70
+ }
71
+ & > *:nth-child(n + 8) {
72
+ animation-delay: 0.46s;
73
+ }
74
+ }
75
+
76
+ /* Медленное покачивание декоративной иллюстрации (маскот на карточках). */
77
+ @keyframes float-slow {
78
+ 0%,
79
+ 100% {
80
+ transform: translateY(0);
81
+ }
82
+ 50% {
83
+ transform: translateY(-6px);
84
+ }
85
+ }
86
+ @utility animate-float {
87
+ animation: float-slow 6s ease-in-out infinite;
88
+ }
89
+
90
+ /* Расходящаяся волна от центра («сигнал/коннект») — кольцо растёт и тает. Вешать на
91
+ абсолютные span'ы позади иконки (напр. галочка успеха оплаты); два span'а со сдвигом
92
+ animation-delay дают непрерывную пульсацию. */
93
+ @keyframes signal-pulse {
94
+ 0% {
95
+ transform: scale(0.85);
96
+ opacity: 0.55;
97
+ }
98
+ 80%,
99
+ 100% {
100
+ transform: scale(2);
101
+ opacity: 0;
102
+ }
103
+ }
104
+ @utility animate-signal-pulse {
105
+ animation: signal-pulse 1.9s cubic-bezier(0.22, 1, 0.36, 1) infinite;
106
+ }
107
+
108
+ /* Кружок-вердикт (галочка успеха/алерт): упругое появление + естественное «дыхание».
109
+ Две анимации в одном свойстве: pop конечный (both, с overshoot), breathe — мягкая
110
+ бесконечная пульсация, стартует с задержкой = длительности pop. Волны позади
111
+ (animate-signal-pulse) дышат в том же ритме визуально. */
112
+ @keyframes verdict-pop {
113
+ from {
114
+ opacity: 0;
115
+ transform: scale(0.75);
116
+ }
117
+ to {
118
+ opacity: 1;
119
+ transform: scale(1);
120
+ }
121
+ }
122
+ @keyframes verdict-breathe {
123
+ 0%,
124
+ 100% {
125
+ transform: scale(1);
126
+ }
127
+ 50% {
128
+ transform: scale(1.05);
129
+ }
130
+ }
131
+ @utility animate-verdict-pop {
132
+ animation:
133
+ verdict-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both,
134
+ verdict-breathe 2.6s ease-in-out 0.5s infinite;
135
+ }
136
+
137
+ /* ── Modal blur-in ─────────────────────────────────────────────────────────
138
+ Появление модалки «из блюра» (blur + scale + fade) — контент проявляется, как при
139
+ наведении фокуса камеры. Центрирование делает флекс-обёртка (см. modal.tsx), поэтому
140
+ в keyframes только scale — НЕ трогаем translate, иначе анимация перебивала бы
141
+ центрирование и модалку уносило в угол. Выход — короче (0.2s) и с меньшим блюром,
142
+ чтобы текст не «мылился» дольше необходимого. */
143
+ @keyframes modal-blur-in {
144
+ from {
145
+ opacity: 0;
146
+ filter: blur(12px);
147
+ transform: scale(0.96);
148
+ }
149
+ to {
150
+ opacity: 1;
151
+ filter: blur(0);
152
+ transform: scale(1);
153
+ }
154
+ }
155
+ @keyframes modal-blur-out {
156
+ from {
157
+ opacity: 1;
158
+ filter: blur(0);
159
+ transform: scale(1);
160
+ }
161
+ to {
162
+ opacity: 0;
163
+ filter: blur(8px);
164
+ transform: scale(0.97);
165
+ }
166
+ }
167
+ @utility modal-blur-in {
168
+ animation: modal-blur-in 0.35s cubic-bezier(0.22, 1, 0.36, 1) both;
169
+ }
170
+ @utility modal-blur-out {
171
+ animation: modal-blur-out 0.2s ease-in both;
172
+ }
173
+
174
+ /* ── Reduced motion ────────────────────────────────────────────────────────
175
+ Уважаем системную «меньше движения»: срезаем ВСЕ анимации/переходы (tw-animate-css,
176
+ наши reveal/float, hover-lift) — контент сразу в финальном состоянии, без движения. */
177
+ @media (prefers-reduced-motion: reduce) {
178
+ *,
179
+ *::before,
180
+ *::after {
181
+ animation-duration: 0.01ms !important;
182
+ animation-iteration-count: 1 !important;
183
+ transition-duration: 0.01ms !important;
184
+ scroll-behavior: auto !important;
185
+ }
186
+ }
187
+
188
+ /* Адаптивные size-классы (брейкпоинт-префиксы для responsiveSize) генерятся из
189
+ литералов в src/lib/responsive-sizes.keep.ts — Tailwind сканит его как обычный
190
+ исходник (умеет arbitrary `[…]` и варианты `data-[…]`, в отличие от @source inline). */
191
+
192
+ /* ── View Transitions: смена темы ──────────────────────────────────────────
193
+ Тему переключаем круговым «раскрытием» из точки клика по тумблеру (см. theme-switch.tsx:
194
+ clip-path-анимация ::view-transition-new(root)). Дефолтный crossfade корня убираем — иначе
195
+ он наложится на clip-path и даст грязное подмигивание. Новый слой поверх старого (z-index),
196
+ старый статичен под растущим кругом нового. */
197
+ ::view-transition-old(root),
198
+ ::view-transition-new(root) {
199
+ animation: none;
200
+ mix-blend-mode: normal;
201
+ }
202
+ ::view-transition-new(root) {
203
+ z-index: 1;
204
+ }
205
+ ::view-transition-old(root) {
206
+ z-index: 0;
207
+ }