UI kit · v1.0 · 30.10.2026

Every control,
every state

The components every Nomero app shares: the PMS, ID, Chat, the console and hotel sites. One stylesheet, ui.css, draws all of them. Apps use these class names and never restyle a component locally.

  • Day and Night
  • Works with reka-ui and Radix
  • Plain HTML, Vue, React

Rules every component follows

These rules keep five apps built by different people looking like one product.

  • Three sizes, one grid. Small is 32px (tables, toolbars, filters), medium is 40px (the default), large is 48px (hotel sites, phones, the one main action). A button and an input of the same size always line up.

  • Variants are classes, states are attributes. Write nm-btn nm-btn-ghost nm-btn-sm. Never add a class for a state; set disabled, aria-invalid, aria-busy, aria-current, or let reka-ui and Radix set data-state. The look follows the accessibility state, so the two cannot disagree.

  • Every control has all six states. Hover, pressed, focus, disabled, busy and invalid. Focus is always a cobalt ring drawn 2px off the edge.

  • Inputs use 16px text. Smaller text makes phones zoom on focus. Only the small size, used in dense tables and toolbars, drops to 14px.

  • No spinners. Waiting is shown with the window loader, a pen stroke under a busy button, or hatching shaped like what is coming.

  • Drawn, not decorated. Hairlines instead of shadows. Anything a draftsman would letter (table heads, title blocks, breadcrumbs, captions) is mono uppercase. Every number is set in Geist Mono.

  • One primary action per view. The cobalt pill appears once. Everything else is an outline or a ghost.

  • Missing something? Add it to ui/nomero-ui.css, rebuild, and it reaches every app. Never fork a component inside an app.

Drawing marks

The pieces that make a screen read as a sheet from a drawing set rather than a generic dashboard. Use them for structure and labels, never as ornament.

Гостиница Алатау · Брони

Заезд · 30.10 · 14:00
ПровереноОплаченоЖдём оплатуОвербукингVIP
Eyebrow with the drafting diamond, mono caption, stamps for short verdicts
Отель
Алатау
Лист
Этаж 4
Масштаб
1:100
Занято
этаж 9 из 13 · отель 36 из 41
Title block: mono keys and values, hairlines from 1px gaps
Счёт · Бронь #482130.10.2026
Проживание, 4 ночи
114 000 ₸
Поздний выезд
9 600 ₸
Оплачено с сайта
57 000 ₸
К оплате
66 600 ₸
Featured sheet with a cobalt strip; spec list with dotted leaders for folios and reports
Шахматка, неделя 441440 × 900
Plate: crop marks around a screenshot, photo or drawing, with a mono caption
WhatsAppДелюкс · №31272 000 ₸Айгерим Ж. · 3 → 5 ноя
Callout note: what a leader line points at on a plan, a chart or the room grid
ЗабронированГость в номереВыехалУборка или ремонтОтменено
ОплаченоЧастичноНе оплачено
Legend marks from the floor plan: squares are rooms, circles are money

Buttons

Five variants, all pills. Primary is cobalt and appears once per view. Secondary is an outline that turns cobalt under the pointer. Danger is kept for actions that lose data or money.

Primary, secondary, ghost, danger, danger soft
DefaultHoverPressedFocusDisabledBusy
Busy keeps the width and label, draws a pen stroke along the bottom and sets aria-busy="true". Hover and pressed are simulated here.
nm-btn-sm, default, nm-btn-lg
Icon before a verb, caret after a menu; icon only buttons always carry aria-label
Attached group; toggle button with aria-pressed
Условия отмены и возврата
Full width on phones and hotel sites; inline link with nm-link

Loaders and progress

No spinners. Short waits light the mark's windows in the order you would write the n. Known lengths show a bar. Content shows its own hatched skeleton.

Window loader: inline, default, large, on the tile for app start
Загружаем шахматку
App start and full page waits over one second
Импорт броней214 из 298
Синхронизация с каналамиидёт
Determinate (moves with transform, never width) and indeterminate
Top line for route changes, with skeletons below it

Skeletons

Shaped like the content that is coming, so nothing jumps when it arrives. The hatching drifts like a drawing still being made, and moves with transform only.

Table: real header, placeholder cells
Room card and stat tile

Text fields

Label above, hint or error below, nothing inside the box but the value. Focus draws a cobalt ring. An error turns the border red and says what to fix.

Как в документе
Hover
Focus
Добавьте домен после точки, например mail.ru
Присваивается автоматически
Read only
Default, hover, focus, invalid (aria-invalid with aria-describedby), disabled, read only
+7
₸
Prefix, suffix, leading icon, clear and reveal actions
Гость этого не увидит
Textarea with a live counter; small and large sizes

Checkboxes, radios and switches

Checkboxes pick items, radios pick one option, switches change a setting that applies at once. The label is always clickable.

Отправить гостю
Checked, unchecked with description, indeterminate, invalid, disabled
Способ оплаты
Radio group in a fieldset with a legend
WhatsApp подключён
Native checkbox with role="switch", or a button as reka-ui and Radix render it
15 000 ₸90 000 ₸
Slider, used for price filters on hotel sites

Selects, comboboxes and chips

A select for a short fixed list, a combobox when people type to find, a segmented control for two to four views, chips for filters.

Тип номера, открыт
Стандарт12 номеров
Делюкс8 номеров
Люкс4 номера
СемейныйНет свободных на эти даты
Native select; custom select open with selected, highlighted and disabled options
Combobox: type to filter. Try a name that is not there.
Segmented control for one of a few views; filter chips for any number
VIP Поздний выезд Аллергия на орехи
Enter добавляет пометку
Time slots for hourly stays
Tag input; time slots where taken slots are struck through

Dates, counts, codes and files

The inputs a hotel needs every day. Dates always read as a stay: arrival, departure and the number of nights.

Даты проживания
Range calendar: today carries a cobalt diamond, nights between are tinted. Click two days.
Hotel site version with the nightly price in thousands of tenge; sold out nights are struck through
ВзрослыеОт 14 лет
2
ДетиДо 14 лет, завтрак бесплатно
0
Number stepper; reaching the limit disables that button
Код из приложения
Шесть цифр из Google Authenticator
Код не подошёл. Новый появится через 18 секунд.
One time code for staff sign in; typing moves to the next cell
pasport_zhumabaeva.pdf1,8 МБ · загружен
IMG_2041.HEICЗагружаем, 64%
dogovor_alatau.pdfФайл 14,2 МБ, больше 10 МБ. Сожмите его или разбейте на части.
Drop zone (hover and dragover light up); file rows: done, uploading, failed. Drop a file to try.

Menus, dialogs and drawers

Things that float get the raised surface and the overlay shadow. A dialog asks one question; a drawer shows a record without leaving the page; a sheet is the phone version of both.

Menu: highlighted, disabled with a reason, separator, danger last
Номер 404 на ремонте Замена смесителя до 31 окт. Номер не продаётся на сайте и в каналах.
Цена со скидкой 10% за 3 ночи
Popover for detail with actions; tooltip for a short label only
Destructive confirmation: the question names the record, the danger button repeats the verb
Взрослые
2
Дети
0
Bottom sheet on phones, floating 8px off the edges
Новая броньНомер 402

Новая бронь

Номер 402, Стандарт, 31 окт → 2 ноя

+7

Бронь #4821

Создана 12 окт с сайта отеля

В номереЧастично оплачено
Гость
Айгерим Жумабаева
Номер
402, Стандарт
Даты
29 окт → 2 ноя · 4 ночи
Гости
2 взрослых
Сумма
114 000 ₸, оплачено 57 000 ₸
  1. Гостья заселилась29 окт, 14:12 · Динара, ресепшен
  2. Предоплата 57 000 ₸ получена12 окт, 22:44 · Kaspi
  3. Бронь создана с сайта отеля12 окт, 22:41

Alerts, banners and toasts

Alerts sit next to what they are about and stay until fixed. Banners cover the whole app. Toasts confirm what just happened and leave on their own after five seconds.

Цены уходят в каналы каждые 5 минут

Последняя отправка в 01:05, ошибок нет.

Импорт завершён

298 броней перенесены, расхождений нет.

2 гостя заезжают сегодня без оплаты

Напомните им оплатить до 14:00, иначе номера вернутся в продажу.

Info, success, warning, danger. The title says what happened, the body says what to do.
Нет соединения. Изменения сохранятся, когда интернет вернётся.
Тестовый режим: брони с сайта не уходят в каналы.
App wide banners under the app bar
Бронь #4821 сохранена
Бронь #4790 отменена
Success, undo, error with a fix

Badges, counts, tags and avatars

Status is a legend mark and a word, never color alone. A cobalt count means guests are waiting; an outlined count is just a number.

ПодтвержденоВ номереВыехалОтменено
ОплаченоЧастично оплаченоНе оплаченоБез завтрака
Stay and payment badges; a plain badge for neutral facts
4399+⌘KEsc
BKBooking.comABAirbnbWSСайт отеляWAWhatsAppPHТелефон
Counts, keys, sales channels. Channels use neutral codes, never other companies' logos.
АЖАЖДСТА ДСЕКМО+2
Initials, no stock photos. A green dot means on shift.
36 900 ₸41 000 ₸за ночь
VIPПоздний выезд
Price with the old price struck; removable tags

Tables, cards and stats

Tables carry the work. Numbers align right in tabular figures, names come with initials, the sorted column is marked and a selected row is lit.

ГостьНомер КаналСтатус Действия
АЖАйгерим Жумабаева+7 701 234 56 78
40229 окт → 2 нояWSСайтВ номере114 000 ₸
СБСауле БековаЗаезд сегодня в 14:00
40330 окт → 1 нояBKBooking.comПодтверждено73 800 ₸
ДСДанияр Сериков+7 702 655 13 80
40131 окт → 3 нояPHТелефонНе оплачено85 500 ₸
МОМадина Оспанова2 взрослых, 1 ребёнок
4041 ноя → 5 нояABAirbnbЧастично оплачено147 600 ₸
ЕАЕрлан АбеновОтменил 28 окт
4032 ноя → 4 нояWAWhatsAppОтменено73 800 ₸
Выбрано 2 · 159 300 ₸
Sortable header (aria-sort), selection with a bulk action bar, row actions, cancelled sum struck
Загрузка сегодня87,8%4,2 п.п.к прошлой пятнице
Средняя цена ночи31 640 ₸6,1%за неделю
Выручка за октябрь4 182 300 ₸2,4%к плану
Заезды сегодня72 ещё не оплачены
Stats: mono figures between hairlines, each change with its direction and baseline

Бронь #4821

В номере
Гость
Айгерим Жумабаева
Номер
402
Заезд
29.10
Выезд
02.11
Ночей
4
Сумма
114 000 ₸
Оплачено
57 000 ₸
Record card with a title block, the way a drawing labels its corner

Делюкс с видом на горы

28 м² · двуспальная кровать · до 3 гостей
Завтрак включёнОсталось 2 номера
36 900 ₸41 000 ₸за ночь
Room card for hotel sites. Until a photo is uploaded, the frame shows hatching and its pixel size, like a placeholder on a drawing.
  1. Гостья заселилась в номер 40229 окт, 14:12 · Динара, ресепшен
  2. Поздний выезд до 15:00 добавлен30 окт, 10:05 · Ерлан, ресепшен
  3. Предоплата 57 000 ₸ получена12 окт, 22:44 · Kaspi
  4. Списание с карты отклонено12 окт, 22:42 · банк гостя
  5. Бронь создана с сайта отеля12 окт, 22:41
History: newest first, who and when on every line
List rows for people and settings

Chat

Guest messages on the left on paper with their channel in mono, staff replies on the right in cobalt, system notes in the middle in mono.

Сегодня
WhatsAppЗдравствуйте! Можно ли завтра выехать попозже, часов в 15?09:41
Добрый день, Айгерим. Поздний выезд до 15:00 возможен, доплата 9 600 ₸. Оформить?09:44
Да, оформите, пожалуйста09:52
Поздний выезд добавлен в бронь #4821 · Ерлан
Готово. Счёт на 9 600 ₸ отправили отдельным сообщением.09:53
Ссылка на оплату: pay.nomero.kz/4821Не доставлено ·
Incoming, outgoing (sent, read, failed with retry), system note, day divider, composer

Empty and error states

No blank panels. An empty view says why it is empty and offers the next step. An error page says what broke and what to do, and the mark's unlit windows stand in for the missing content.

Сегодня заездов нет

Следующий заезд 31 октября: Данияр Сериков, номер 401.

Nothing to do today

Никого не нашли

По запросу «Ержан Ахметов» гостей нет. Проверьте написание или добавьте гостя.

No search results

Добавьте первый номер

Номера появятся в шахматке, и их можно будет продавать на сайте и в каналах.

First run

Чатов пока нет

Подключите WhatsApp отеля, и сообщения гостей появятся здесь вместе с их бронями.

Feature not set up yet

Error pages

Нет света в этом окне

Такой страницы нет. Возможно, ссылка устарела или в ней опечатка.

На главную
Лист
404
Статус
Не найден
404

Сервер не отвечает

Мы уже знаем о проблеме. Ваши данные в безопасности, обновите страницу через минуту.

Лист
500
Время
30.10 01:12
500

Нет доступа к отелю «Алатау»

Вы вошли как dinara@alatau.kz. Попросите администратора отеля добавить вас в сотрудники.

403

Нет соединения

Шахматка показывает данные на 01:05. Новые брони появятся, когда интернет вернётся.

Offline

Page layout

Every screen opens with the same header: where you are, what this is, and the one main action on the right. Forms group fields under short headings; settings put the switch on the right.

  1. Гостиница Алатау
  2. Брони

Брони

298 броней в октябре, 12 на сегодня

Page header with breadcrumbs and actions; filter toolbar in the small size

Гость

Form that validates on submit; try saving it empty
Подтверждение в WhatsAppСразу после создания брони
Напоминание об оплатеЗа сутки до заезда, если бронь не оплачена
Ночной аудитЗакрывает день в 03:00 и переносит неоплаченное
Settings rows: title and effect on the left, switch on the right

Using it in code

Import the stylesheet once. Wrap each component in the app's framework: behavior comes from reka-ui or Radix, the look comes from these classes.

Every app, once
@import "../brand/dist/fonts.css";
@import "../brand/dist/tokens.css";
@import "../brand/dist/ui.css";
/* Tailwind apps also: */
@import "../brand/dist/tailwind.css";
Vue with reka-ui (idp-front, messenger-front)
<SelectTrigger class="nm-input">
  <SelectValue placeholder="Тип номера" />
  <PhCaretUpDown class="nm-icon" />
</SelectTrigger>
<SelectContent class="nm-menu">
  <SelectItem class="nm-menu-item" value="std">
    Стандарт
  </SelectItem>
</SelectContent>
React with Radix (admin)
<DropdownMenu.Content className="nm-menu">
  <DropdownMenu.Item className="nm-menu-item">
    <PencilSimple className="nm-icon" /> Изменить
  </DropdownMenu.Item>
  <DropdownMenu.Item className="nm-menu-item nm-menu-item-danger">
    Удалить
  </DropdownMenu.Item>
</DropdownMenu.Content>
A thin wrapper keeps variants typed
<!-- NmButton.vue -->
<script setup lang="ts">
defineProps<{
  variant?: 'primary' | 'secondary' | 'ghost'
          | 'danger' | 'danger-soft'
  size?: 'sm' | 'md' | 'lg'
  busy?: boolean
}>()
</script>
<template>
  <button :class="['nm-btn',
      variant && variant !== 'primary' && `nm-btn-${variant}`,
      size && size !== 'md' && `nm-btn-${size}`]"
    :aria-busy="busy || undefined">
    <slot />
    <span v-if="busy" class="nm-dots" aria-hidden="true"><i/><i/><i/></span>
  </button>
</template>
  • reka-ui and Radix set the state. data-state="open", data-highlighted, data-disabled and aria-selected are already styled, so wrappers pass only the class.

  • pms-front uses Headless UI. Give its components the same classes and set aria-selected or data-highlighted from its slot props.

  • Tailwind stays for layout. Use utilities for gaps, grids and widths around components, never for a component's own colors, radius or height.