@temirtator/kazmaps-design-system
v2.1.0
Published
KazMaps design system — one UI kit for every KazMaps brand (Figma tokens, components, icons) for Tailwind v4 + React 19 apps
Readme
@temirtator/kazmaps-design-system
Дизайн-система KazMaps: один UI-кит для всех продуктов — KazMaps, KazMaps Business, KazMaps
Booking и KazMaps Developer Hub. Компоненты сняты с макета Figma «KazMaps Design System»
пиксель в пиксель и сверяются с ним автоматически (npm run test:figma); бренд меняет только
значения токенов, не компоненты. React 19, Tailwind CSS v4, TypeScript.
Установка
npm i @temirtator/kazmaps-design-system lucide-reactPeer-зависимости:
react^19.0.0react-dom^19.0.0lucide-react^1.0.0qrcode^1.5.4 — необязательный, нужен толькоQrCode
Подключение в Next.js или Tailwind v4 приложение
1. Стили в globals.css
@import "tailwindcss";
@import "@temirtator/kazmaps-design-system/styles/core.css";
@import "@temirtator/kazmaps-design-system/styles/theme.css";
@import "@temirtator/kazmaps-design-system/styles/brands/maps.css";
@import "@temirtator/kazmaps-design-system/styles/kit.css";
@source "../../node_modules/@temirtator/kazmaps-design-system/dist";Файл бренда — один из brands/maps.css, brands/business.css, brands/booking.css,
brands/devhub.css.
Порядок менять нельзя: --ease-standard объявлен и в core.css, и в файле бренда с одной
специфичностью — побеждает тот, что подключён позже, то есть бренд.
theme.css даёт утилиты Tailwind вида bg-background-primary, text-text-secondary,
border-border-primary для всех цветовых ролей; можно писать и напрямую —
bg-(--background-primary).
kit.css — утилиты и keyframes, которые читают компоненты (transition-interactive,
focus-ring, gradient-ring, animate-modal-in, animate-shimmer-placeholder).
2. Атрибуты на корневом элементе
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html data-brand="maps" data-theme="light" lang="ru">
<body>{children}</body>
</html>
);
}data-brand:maps|business|booking|devhub.data-theme:light|dark. Без атрибутаmapsпоказывает светлую тему и следуетprefers-color-scheme;businessиbooking— тёмную;devhub— светлую и системную тему не подхватывает,data-themeставит сам.
Оба атрибута ставятся на <html>: theme.css объявляет --color-* на :root, и при атрибуте
на другом элементе утилиты bg-background-primary перестанут находить значения.
3. Шрифт
Бренд maps использует переменный Inter с весами 400, 450, 500 и 550 через --font-inter.
В Next.js — next/font без списка weight, иначе 450 и 550 не загрузятся:
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin", "cyrillic"], variable: "--font-inter" });Бренды business, booking и devhub держат готовый литеральный стек в --font-sans (Inter,
Lexend, Golos Text соответственно) — специальной настройки загрузчика, как у --font-inter для
maps, им не нужно, шрифт достаточно подключить в приложении.
4. Флаги стран
PhoneInput показывает флаг региона эмодзи — так он нарисован в макете. macOS, iOS и Android
рисуют флаги сами; Windows вместо флага показывает две буквы. Для Windows подключите в
приложении шрифт с флагами один раз при старте:
npm i country-flag-emoji-polyfillimport { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
polyfillCountryFlagEmojis(); // регистрирует шрифт «Twemoji Country Flags» только там, где флагов нетКомпонент ставит этот шрифт первым в стеке, поэтому больше ничего настраивать не нужно.
5. Компоненты
import { Button, TextInput, PlaceRow, useToast } from "@temirtator/kazmaps-design-system";Вход /maps оставлен как реэкспорт корня и будет удалён; новые импорты — из корня.
6. Иконки
Набор иконок макета: 104 заливных глифа в поле 24×24, три раздела — Interface, Map UI, Weather.
import { IconSearchBold, IconStarLight } from "@temirtator/kazmaps-design-system/icons";
<IconSearchBold size={20} />
<IconStarLight title="Рейтинг" />Одноцветные иконки залиты currentColor. Компоненты кита рисуют глифы из этого же набора. Иконки погоды многоцветные: читают роли
--weather-* и на currentColor не реагируют. Без title иконка декоративная и скрыта от
скринридера. ICON_MANIFEST перечисляет набор, раздел и начертание каждой иконки.
Типографика
Heading/Text/Caption в 1.0.0 удалены — вместо компонентов кит-класс kit.css даёт
утилиты для пар кегль/интерлиньяж и весов, встречающихся в макете: type-10-12,
type-12-16, type-14-18, type-14-20, type-16-20, type-20-24, type-32-40 и
weight-400, weight-450, weight-500, weight-550.
<p className="type-16-20 weight-500 text-(color:--text-primary)">Профиль</p>Компоненты
Действия: Button (accent | neutral | danger), IconButton (Map Action, 36), IconButtonGroup (зум), Chip (neutral | info | success | warning | danger — три последних построены на ролях tag-*, в макете не нарисованы), Toggle,
PillTabs, MenuTabs, NavItem, Avatar.
Поля: TextInput, PasswordInput, SearchInput (обводка, иконка и ввод; отправку вешает
потребитель — onKeyDown у поля или <form> вокруг него), PhoneInput, CodeInput,
SelectField, DayPicker, Textarea, Checkbox, FieldMessage (подпись под полем через
aria-describedby; три последних в макете не нарисованы — docs/figma-deltas.md).
Списки: Pagination (номера страниц, окно с многоточиями; в макете не нарисован).
Оверлеи: Dialog, NonmodalDialog, BottomSheet, ToastProvider + useToast, Menu +
MenuItem + MenuDivider.
Карта: MapCompass (клик возвращает на север; с heading поворотом управляет приложение), MapTrafficBadge, WeatherBadge, ForecastCard, ScaleBar, NavBar,
CollapseHandle.
Содержимое: PlaceRow, ListRow, SectionHeader, EmptyState, ShimmerBlock, StarRating,
LogoLockup, LegalLink, InDevelopment, QrCode.
Хелперы: cn, useFocusTrap, isTopmostTrap, REGIONS, findRegion, parseE164,
formatE164, toE164, isKazakhstanMobile.
Ошибки — только тостом
Ни одно поле не рисует текст ошибки под собой: поля принимают булев invalid, а текст
показывает useToast. Так во всех продуктах KazMaps.
const show = useToast();
<TextInput value={v} onChange={setV} invalid={!valid} />;
if (!valid) show("Введите номер полностью");PhoneInput
<PhoneInput label="Номер телефона" onChange={({ e164, complete }) => complete && send(e164)} />| Проп | Тип | Описание |
| ----------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| value | string | E.164 ("+77012345678") или "" — контролируемый режим. |
| defaultValue | string | Начальное E.164 для неконтролируемого режима. |
| defaultRegion | RegionCode | Регион, когда value/defaultValue пусты; по умолчанию "KZ". |
| onChange | (value: PhoneValue) => void | На каждое изменение; value.e164 пуст, пока номер не введён полностью. |
| onRegionChange | (region: RegionCode) => void | Только при смене региона — выбор в пикере или вставка номера с другим кодом страны. |
| onFocus, onBlur | FocusEventHandler | Проксируются на внутренний <input>. |
| regions | RegionCode[] | Ограничивает список регионов. |
| locale | "ru" \| "en" | Язык названий стран; по умолчанию "ru". |
| label | string | Подпись над полем; пустая строка или пробелы — без подписи. Подсказок и текста ошибки под полем нет. |
| invalid | boolean | Красная обводка и aria-invalid; текст ошибки — тостом. |
| required, disabled, readOnly | boolean | |
| id, name, autoFocus, autoComplete | | Как у обычного <input>; autoComplete по умолчанию "tel-national". |
| labels | Partial<PhoneInputLabels> | Тексты пикера: имя кнопки региона, плейсхолдер поиска, заголовки групп, пустой результат. |
| className | string | Класс корневой обёртки. |
| ref | Ref<HTMLInputElement> | Ссылка на внутренний <input>: ref={field.ref} у react-hook-form фокусирует поле с ошибкой. |
| aria-describedby | string | Связывает поле с текстом ошибки, выведенным рядом с ним. |
| aria-label | string | Имя поля без видимой подписи; при непустом label не применяется. |
| aria-labelledby | string | Применяется всегда. Видимая подпись имеет id <id>-label — сошлитесь на неё, чтобы собрать составное имя. |
- Селектор показывает флаг, код страны набран текстом в поле: «+7 701 234 56 78». Пикер — все
страны с поиском, точные маски для СНГ, остальным цифры группами и проверка длины E.164. Вставка
+998…переключает регион сама;8 701…нормализуется в KZ. - Код
+1общий для нескольких стран;parseE164и вставленный+1…по умолчанию даютUS. - React Hook Form — через
Controller:onChange={(v) => field.onChange(v.e164)}иref={field.ref}, чтобы форма могла поставить фокус в поле с ошибкой. - Контролируемый
value=""не очищает частично введённое поле: компонент пересинхронизируется только когда внешнийvalueотличается от последнего отправленного E.164 — иначе поле дёргалось бы во время набора. localeпереключает только названия стран; остальные тексты пикера — изlabels, дефолты русские.
Токены
Контракт один на все бренды: имена ролей — имена Figma Variables (группа/роль →
--группа-роль).
- Цвета по темам:
action/*,background/*,overlay/*,border/*,text/*,icon/*,tag/*,traffic/*,weather/*,map/*;effect/shimmer-peak. - Статические:
dimension/corner-radius/*,dimension/height|width/*,spacing/gap|padding/*,stroke/*,shadow/blur|position/*,typography/font-size|line-height/*,font-sans,font-weight-*,text-xs…3xl, тени и тайминги кита (shadow-hud,motion-panel,ease-standardи другие).
Источник истины — tokens/schema.json и tokens/brands/*.json; CSS и docs/tokens.md
генерирует npm run tokens:build. Бренд maps импортируется из tokens/figma/export.json
командой npm run tokens:import; business, booking и devhub пока переходные (_pending
в файле бренда) — docs/brands.md.
Чего не хватает в макете — docs/figma-deltas.md. Какой компонент макета каким стал в коде —
docs/figma-map.md. Как кит сверяется с макетом — docs/figma-compare.md; там же
таблица «Пропуски с дельтами» — всё, что осталось несверенным.
Обводки повторяют strokeAlign макета и места не занимают: внутренние рисуются inset-ring,
градиентное кольцо Button — отдельным слоем gradient-ring, внешние — ring. Высоты
совпадают с макетом: Chip и SelectField 28, IconButton 36, TextInput, PhoneInput и
SearchInput 36, Button 40, ячейка CodeInput 48, PlaceRow 72. У Avatar и MapCompass
коробка 36, а белый круг 40 нарисован внутри и вылезает по 2 px — так объявлено в макете.
Переопределение токенов в приложении
Селектор должен быть не слабее селектора бренда [data-brand="…"] (0,1,0), иначе значения
бренда победят:
:root[data-brand] {
--font-sans: var(--font-inter, "Inter"), system-ui, sans-serif;
}--font-inter должен быть объявлен загрузчиком шрифтов — голый var() на несуществующую
переменную инвалидирует весь --font-sans.
Разработка
Storybook
У каждого компонента две истории: All Variants — все состояния макета статикой, и
Playground — один экземпляр с контролами. Переключатели бренда × темы в тулбаре действуют на
любую историю. По умолчанию канва — фон страницы Figma (#a7a7a7 + белый 25 %), чтобы сравнивать
компонент с макетом один в один; переключатель Canvas возвращает фон бренда:
npm run storybookОпубликованная версия: https://temirtator.github.io/kazmaps-design-system/
Проверки
npm run lint
npm run typecheck
npm run test # tokens:check + check:tokens + vitest
npm run test:vrt # скриншоты Storybook, Playwright
npm run test:e2e # взаимодействия PhoneInput
npm run test:figma # сверка историй с макетом по снапшоту figma/ds
npm run buildИконки из Figma
Нужен персональный токен Figma с областью file_read в переменной FIGMA_TOKEN или в
локальном .env:
npm run icons:import
npm run icons:buildИмпорт заменяет src/icons/svg только после того, как все глифы скачаны. Слои с
подчёркиванием в начале имени в набор не входят. npm run icons:check падает, если
сгенерированные компоненты разошлись с исходными SVG.
Снапшот и сверка с Figma
Бинарь пакета kazmaps-figma умеет четыре команды: snapshot (снять файл Figma в репозиторий),
expect (напечатать ожидания сверки по ноде снимка), codemod-tokens и check-tokens (миграция
клиента на контракт токенов — два раздела ниже). Те же функции доступны экспортом
@temirtator/kazmaps-design-system/figma-tools.
figma/ds/ — закоммиченный снимок файла «KazMaps Design System»: tree.json (обрезанное
дерево нод), manifest.json (каждый вариант компонента: id, свойства, размер, рендеры),
renders/** (PNG в 1×). Обновляется отдельным PR «design bump»:
zsh -ic 'npm run figma:snapshot' # FIGMA_TOKEN из профиля шеллаТот же снапшот доступен как бинарь пакета — npx kazmaps-figma snapshot --file=ds — и как
экспорт @temirtator/kazmaps-design-system/figma-tools (snapshot, figmaFetch, trimNode,
slug, variantSlug, expectationsFor, codemodTokens, checkTokens) для скриптов вне
репозитория. Прогон делает 6 запросов к API (файл + 5 батчей рендеров) и затем 231
последовательную загрузку PNG с CDN с 5-секундной паузой между батчами — в сумме около минуты.
Ревью PR «design bump» смотрит на manifest.json: sha256 меняется только у тех рендеров,
что правда поменялись в Figma, так что диф по манифесту и есть список изменений — сами PNG
открывать не нужно.
Сверка кода с макетом — npm run test:figma (vrt/figma.spec.ts, карта vrt/figma-map.ts):
для каждой записи карты (нода Figma ↔ история Storybook ↔ селектор) слой 1 сверяет числа —
размеры, паддинги, gap, радиус, заливку, боксы именованных слоёв (data-figma="<имя слоя>") и
типографику текстов; слой 2 гоняет pixelmatch рендера истории против PNG из снапшота и считает
кластеры отличий вне текста; слой 3 — тест покрытия, который падает, пока в карте не хватает
варианта из manifest.json (иконки не в счёт, их держит icons:check). Отчёт с тремя картинками
на вариант (макет, код, диф) — test-results/figma-report/index.html. Правила замера (a–g) и
таблица «Пропуски с дельтами» — всё, что осталось несверенным и почему — в
docs/figma-compare.md.
Что слой 1 ждёт от конкретной ноды, печатает expect — с этого удобно начинать новую запись
карты:
npx kazmaps-figma expect --node=147:264 --layers="Button Text" # Button, State=Default/AccentCodemod токенов у клиентов
Клиенты (business-client, booking-client, developer-hub-client) ещё держат имена токенов
0.x — --ink, --muted, --line, --brand, --card, --bg, --radius-*, --danger… Таблица
переименований — scripts/figma/token-map.json: any — замены, одинаковые для всех брендов,
byBrand — радиусы (у каждого бренда своя шкала отступов), manual — токены без механической
замены, потому что старое имя расходится на несколько ролей кита в зависимости от контекста
(например --danger — это то --text-danger, то --action-danger-primary, то --tag-red).
npx kazmaps-figma codemod-tokens --root=src --brand=business # отчёт, файлы не тронуты
npx kazmaps-figma codemod-tokens --root=src --brand=business --write--brand — один из business, booking, devhub, maps; неизвестный бренд — usage и код
возврата 1. У devhub --radius-lg — ближайшее совпадение шкалы (14px между 12 и 16), а не
точное значение, — решение дизайнера открыто в дельте №31 (docs/figma-deltas.md). Без --write печатается только
JSON-отчёт: changed — какие файлы и сколько замен из any/byBrand было бы сделано, manual —
где в коде встретились токены без автозамены (файл, строка, токен, подсказка какую роль кита
выбрать вместо него) — эти места правит руками тот, кто делает миграцию. Функция доступна и как
экспорт — codemodTokens из @temirtator/kazmaps-design-system/figma-tools.
check-tokens: гейт на старые токены и сырой hex
Гейт ищет по --root (src/**/*.{ts,tsx,css,mdx} по умолчанию) старые имена токенов — все ключи
token-map.json (any, byBrand, manual) — и сырые hex-цвета (#fff, #ffffff, #ffffff80…),
которые должны были стать переменными кита.
npx kazmaps-figma check-tokens --root=src --allow=hex-allow.jsonБез находок — выход 0 и пустой вывод. При находках печатает по строке file:line kind match
(kind — token или hex) и завершается кодом 1.
Формат allow-файла (шаблон — scripts/figma/hex-allow.example.json):
{
"files": ["e2e/**", "**/*.stories.tsx", "**/*.test.tsx", "**/*.test.ts"],
"patterns": ["--blob-", "--strip-"]
}files — глобы (**/*) по путям относительно --root, файл при совпадении пропускается
целиком (истории, тесты, e2e-фикстуры). patterns — подстроки: находка на строке, которая их
содержит, не считается (свои переменные лендинга вроде --blob-opacity/--strip-color, которые
не входят в контракт кита и намеренно не токенизируются).
Сам DS гоняет гейт на себе — npm run check:tokens (входит в npm test), конфиг —
scripts/figma/hex-allow.ds.json:
**/*.stories.tsx,**/*.test.tsx,**/*.test.ts— примеры и фикстуры, а не код кита;styles/**— сгенерированные палитры брендов (src/styles/brands/*.css) — это и есть определения hex внутри токенов;docs/**— истории Storybook внутри документации;components/qr-code.tsx—qrcodeкодирует изображение по буквальным#000000/#ffffff, контраст QR обязан оставаться максимальным независимо от бренда и темы — токенизировать здесь нечего;- паттерн
#0f1214— буква вAvatarбез снимка держит фиксированные тёмные чернила поверх любого цветаtag/*(дельта №14,docs/figma-deltas.md), а не токен кита.
Функция доступна и как экспорт — checkTokens из @temirtator/kazmaps-design-system/figma-tools.
Релизы
Версию поднимает релизный PR, тег и публикация — за владельцем.
- Ветка
chore/release-X.Y.Z: секция## [X.Y.Z] — YYYY-MM-DDвCHANGELOG.md, правки документации и версия"X.Y.Z"руками вpackage.jsonиpackage-lock.json(безnpm version). Один коммитchore(release): X.Y.Z, PR с тем же заголовком. - Полные гейты перед ревью — весь блок «Проверки» выше плюс
npm pack --dry-run: в списке пакета должны бытьscripts/figma/*.mjsи все четыреdist/styles/brands/*.css, а__tests__— нет. - После мержа — владелец:
git checkout main && git pull
git tag vX.Y.Z && git push --tags
npm whoami && npm owner ls @temirtator/kazmaps-design-system
npm publish # OTP1.0.0 — один кит на все бренды; business-client и booking-client переезжают с 0.3.0 по
docs/brands.md.
Лицензия
MIT, copyright Temirlan Shagyrov, 2026.
