Лендинг бутика табака и вейпинга. Ультраминималистичный люкс: обсидиан, платина и ни одного лишнего цвета.
Демонстрационная работа. Магазин, тексты и контакты вымышлены. Сайт не имеет юридической силы и сделан как кейс в портфолио.
Второй кейс на одном бренде. Первый, H:\parista, сделан в языке тёмного
неонового глянца: бирюза, стекло, дым, фотографии витрины. Этот собран
с нуля в противоположной эстетике нишевого бутика, и общих элементов
у них нет намеренно.
| Неоновый кейс | Этот | |
|---|---|---|
| Акцент | бирюза #00E5FF |
платина #E6DFD5 |
| Заголовки | Cormorant Garamond, антиква | Unbounded Light, архитектурный гротеск |
| Материал | стекло с размытием и цветной гранью | матовая панель с волосяной металлической гранью |
| Скругления | 18–24 px | 2 px |
| Фон | дым на холсте, точечная сетка | микро-радиальный свет и зерно |
| Секции | чередование тёмных полос | один обсидиан, разделённый волосяными линиями |
| Появление | плавный подъём | штора по маске clip-path |
| Каталог | сетка карточек с фото | бенто без зазоров, блик за курсором |
- Живой статус работы в шапке и в подвале: считается по часам покупателя, обновляется раз в минуту, точка пульсирует вторым кольцом
- Бенто-каталог на шесть разделов без зазоров: плитки разделены самой линией сетки, как швы на корпусе. При наведении описание выезжает снизу, крупный номер уходит глубже, за курсором идёт мягкий платиновый блик
- Мессенджеры реальными ссылками: MAX, WhatsApp с готовым текстом, Telegram
- Подтверждение возраста первым экраном и куки-полоса после него
- Заголовки открываются шторой по
clip-path, блоки поднимаются при прокрутке - Всё движение отключается по
prefers-reduced-motion - Адаптив от 320 px
- Заголовки безопасности: CSP от
default-src 'none',frame-src 'none', HSTS,nosniff. Чужих доменов на сайте нет вообще - Разметка
schema.org/TobaccoShopс часами, контактами и каталогом
Пара здесь нет намеренно: он уже работает на неоновом кейсе, и повтор убил бы то самое отличие, ради которого этот сайт и делался. Вместо частиц взята физика материала.
Металл в заголовке. Буквы залиты не краской, а широким градиентом от тёмной платины к белому и обратно. Узкий блик ходит по ним за курсором, без курсора продолжает ходить сам. Заливка висит на самой строке, а не на заголовке: если поднять её выше, маска открытия перестанет работать, потому что она обрезает элемент, а фон рисовался бы уровнем выше.
Есть @supports на background-clip: text. Где его нет, буквы просто
остаются платиновыми.
Световая полоса. Очень широкая и очень слабая, идёт по всей плоскости за тем же курсором. Читается как лампа, проезжающая над витриной.
Знак за текстом. Фирменный знак ростом в 900 пикселей, непрозрачность 3%, уходит за правый край. Сносится в сторону, обратную курсору: получаются две плоскости вместо одной, и появляется глубина без единой фотографии.
Всё это один цикл requestAnimationFrame на весь экран. Значения
не присваиваются напрямую, а догоняются: резкий блик выдаёт скрипт,
плавный читается как отражение. Курсор пропал на две секунды, и цели
начинают ходить по синусу сами, чтобы экран не замирал.
По prefers-reduced-motion цикл не запускается вовсе, полоса и знак
скрываются, металл остаётся неподвижным.
Текстура лежит на всей странице одним закреплённым слоем: мелкое зерно плюс очень редкая диагональная штриховка. Вместе они читаются как брашированный металл под краской, по отдельности почти незаметны.
Слой закреплён по экрану, а не по документу. Поверхность стоит на месте, пока контент едет мимо, и страница ощущается лежащей за стеклом. Уровень подобран так, чтобы окно 18+ и куки остались над ним чистыми.
Ни одной картинки: зерно нарисовано фильтром прямо в data: URI,
штриховка это повторяющийся градиент.
Слева висит оглавление из пяти пунктов. Показывает, где человек сейчас, и даёт прыгнуть в любой раздел. Активная позиция подсвечена бронзой, название разворачивается по наведению.
Активным считается раздел, чей верх последним прошёл треть экрана.
IntersectionObserver тут не годится: на длинных секциях в поле зрения
их сразу две, и он честно рапортует про обе. Поэтому меряем сами,
но не чаще одного раза в кадр.
Шкала появляется только после первого экрана и прячется, когда окно уже и места слева не остаётся.
Карта грузится сама, метка стоит по координатам. Поиск по строке адреса Яндекс понимает вольно и подсовывает соседние организации, поэтому передаём готовую точку. Порядок в параметрах у него долгота, потом широта.
Виджет приходит светлым и на обсидиане выглядит дырой в стене. Он проходит через инверсию с поворотом тона: подложка уходит в тёмное, подписи становятся светлыми. Насыщенность душится почти в ноль, остаток уводится в тёплое, иначе после инверсии красные значки Яндекса становятся бирюзовыми, а бирюза здесь запрещена.
Рядом лежит шкала суток. Светлая часть это рабочие часы, метка показывает текущий момент и подсвечивается бронзой, пока магазин открыт.
| Роль | Цвет |
|---|---|
| Фон | обсидиан #0A0C0E |
| Панель | #0C0E11 → #101317 градиентом |
| Панель под курсором | #151920 |
| Текст | платина #E6DFD5 |
| Второстепенный | платина 70% и 45% |
| Линии | платина 9% и 16% |
| Номера разделов, точка статуса | бронза #B5936A |
Бирюзы нет ни в одном месте: она осталась в прошлом кейсе.
Заголовки: Unbounded, начертания 200 и 300. В брифе стояли Syne,
Cabinet Grotesk и Monument Extended, но у первого на Google Fonts только
латиница, второй живёт на Fontshare, третий коммерческий. На русском
заголовке любой из них отвалился бы на системный шрифт. Unbounded это
ближайший живой аналог Monument Extended с полной кириллицей.
Текст: Inter. Оба шрифта вшиваются при сборке, к Google браузер не обращается ни разу.
Капса в заголовках нет: широкий гротеск в верхнем регистре мгновенно превращается в вывеску. Верхний регистр остался только в подписях разделов и там работает подписью, а не заголовком.
parista-lux/
├── src/
│ ├── app/
│ │ ├── layout.tsx шрифты, метаданные, восстановление состояния
│ │ ├── page.tsx сборка страницы и разметка для поисковиков
│ │ ├── globals.css токены, типографика, панель, кнопки
│ │ └── icon.svg фавикон
│ │
│ ├── components/
│ │ ├── Header.tsx шапка со статусом работы
│ │ ├── Status.tsx живой индикатор «открыто до 23:00»
│ │ ├── Hero.tsx первый экран, свет и зерно
│ │ ├── Manifest.tsx блок «кто мы»
│ │ ├── Catalog.tsx бенто-сетка с бликом за курсором
│ │ ├── Why.tsx три причины
│ │ ├── Footer.tsx связь, реквизиты, дисклеймеры
│ │ ├── Gate.tsx окно 18+ и куки
│ │ ├── Reveal.tsx появление при прокрутке
│ │ └── Icons.tsx значки одной линией
│ │
│ └── lib/content.ts тексты, контакты, состав каталога
│
├── next.config.ts заголовки безопасности
└── dev.bat / start.bat запуск без Node в системном PATH
| Задача | Файл |
|---|---|
| Телефон, адрес, часы работы | src/lib/content.ts → shop |
| Ссылки на мессенджеры | src/lib/content.ts → links |
| Разделы каталога и размер плиток | src/lib/content.ts → catalog |
| Три причины | src/lib/content.ts → why |
| Тексты окна 18+ и куки | src/lib/content.ts → ui |
| Цвета, шрифты, материал | src/app/globals.css |
Размер плитки задаётся полем size: hero занимает две колонки и два ряда,
wide две колонки, unit одну. Порядок в массиве и есть порядок на сетке.
Знак пришёл фотографией подсвеченной вывески с прилавка: белое свечение на серо-синем фоне. Просто обрезать такой снимок нельзя, поэтому он обведён в кривые.
Как это сделано. Снимок увеличен вчетверо, порезан порогом по яркости (разрыв между знаком и фоном там в полторы сотни уровней, так что порог устойчив), связные области размечены обходом в ширину, границы обведены по Муру, ступеньки спрямлены Рамером и Дугласом, углы срезаны Чайкиным и переписаны кубическими кривыми. Получилось четыре контура и 5.6 КБ пути.
| Файл | Что это |
|---|---|
src/components/Mark.tsx |
знак в разметке, красится через currentColor |
src/app/icon.svg |
вкладка браузера: знак на обсидиановой плашке |
src/app/favicon.ico |
семь размеров в одном файле, от 16 до 256 |
src/app/apple-icon.png |
180, для домашнего экрана |
public/logo.svg |
чистый вектор без плашки |
public/logo-white.png |
белый знак на прозрачном, 1024 |
У мелких размеров иконки поле меньше, а скругление плашки слабее: на 16 пикселях каждый лишний пиксель отступа съедает штрих знака.
Слоты уже впаяны и ждут файлов. Пока файла нет, плитку держат крупный номер, свет и грани, а широкая полоса в блоке «Кто мы» не рисуется вовсе. Положили файл, он проявился. Ни скачков, ни битых рамок: макет одинаков в обоих состояниях.
Формат везде JPEG, цветовой профиль sRGB, качество 80–85.
Кладутся в public/img/catalog/. Имя файла совпадает с id раздела
в content.ts, менять его нельзя.
| Файл | Раздел | Размер | Что в кадре |
|---|---|---|---|
shisha-tobacco.jpg |
Табак для кальяна | 1800 × 1600 | стопка банок под углом, одна открыта |
hookahs.jpg |
Кальяны | 1600 × 1000 | один аппарат целиком, силуэтом |
pods.jpg |
POD-системы | 1000 × 1000 | два-три устройства, одно в руке |
disposables.jpg |
Одноразки | 1000 × 1000 | ряд корпусов торцом, уходящий в расфокус |
flavours.jpg |
Ароматизаторы | 1600 × 1000 | флаконы строем, свет сзади через стекло |
parts.jpg |
Комплектующие | 1600 × 1000 | чаша, калауд и уплотнители россыпью |
| Файл | Путь | Размер | Что в кадре |
|---|---|---|---|
interior.jpg |
public/img/ |
2400 × 1030 (21:9) | зал во всю ширину, витрины по бокам |
og.jpg |
public/ |
1200 × 630 | знак и название на обсидиане, для соцсетей |
Фон однотонный тёмный, ближе к обсидиану, чем к чёрному. Свет мягкий боковой или контровой, одна ключевая точка плюс отражатель. Никакого цветного неона: он подерётся с платиной и уронит всю концепцию.
Кадр строим с воздухом, предмет мельче, чем хочется: снимок в плитке приглушён до 34% и перекрыт затемнением снизу, крупный объект в упор превратится в пятно. Композицию держим по центру или чуть выше, низ кадра уходит под подпись.
Вертикальные кадры не подойдут: все слоты горизонтальные или квадратные.
Он весит примерно вдвое меньше. Расширение задано в двух местах, поменять
надо оба: src/components/Catalog.tsx (строка со Shot) и
src/components/Manifest.tsx (строка с Band).
Node.js 18 или новее. Сайт поднимается на порту 3100, чтобы не спорить с соседним проектом на 3000.
npm install && npm run devБоевая сборка:
npm run build && npm startНа Windows без Node в PATH используйте ярлыки dev.bat и start.bat.
Route (app) Size First Load JS
┌ ○ / 4.67 kB 107 kB
├ ○ /_not-found 993 B 104 kB
└ ○ /icon.svg 0 B 0 B
○ (Static) prerendered as static content
Зависимостей ровно три: next, react, react-dom. UI-библиотек, Tailwind
и анимационных пакетов нет, всё поведение написано вручную.
- Предметной съёмки нет, каталог держится на типографике
- Прайса нет намеренно: бутик показывает ассортимент, а не ценник
- Смены тем нет: концепция построена на одном обсидиане, светлый вариант размыл бы её
- Ссылка на MAX ведёт по номеру телефона, а не в конкретный чат
Разворачивается на Vercel без настроек. После деплоя впишите выданный адрес
в SITE_URL в src/lib/content.ts.
MIT