fix(site): мобильные и UX-правки по тесту (Android/iOS), типографика KB/блога

- Шапка: десктоп-меню с xl (чтобы на ~1050px не ломалось в 2 строки); CTA без
  переноса; в мобильной панели добавлены «Интеграции», «Статус», «Что нового»;
  Android «Назад» закрывает панель (pushState/popstate), Esc/тап/крестик остаются.
- Конструктор «Бизнес»: ползунки удобнее на тач — крупная ручка, touch-action
  pan-y (вертикальный скролл не двигает значение), кнопки −/+; бейдж
  «Популярный» (как на главной) и «90 дней бесплатно».
- Кнопка «Настроить тариф» на главной → /pricing#configurator со скроллом к
  конструктору; на /pricing убран ложный пункт «Импорт из других систем» у «Старт».
- Типографика markdown (h2/h3, списки, таблицы, ссылки) вынесена в global.css —
  /kb/* больше не плоский текст; эмодзи-фолбэк; увеличенная зона нажатия у
  инлайн-ссылок; снят дублирующий inline-стиль в blog/[slug].
- Футер: «Status» → «Статус».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-11 01:22:14 +05:00
parent 5f2188a7f9
commit 6f229ebecc
6 changed files with 84 additions and 33 deletions

View file

@ -10,16 +10,24 @@ interface SliderProps {
label: string; value: number; min: number; max: number; onChange: (v: number) => void; suffix?: string
}
function Slider({ label, value, min, max, onChange, suffix }: SliderProps) {
const dec = () => onChange(Math.max(min, value - 1))
const inc = () => onChange(Math.min(max, value + 1))
const btn = 'w-9 h-9 shrink-0 rounded-md border border-slate-300 text-xl leading-none text-slate-700 flex items-center justify-center hover:bg-slate-50 disabled:opacity-40 disabled:hover:bg-transparent'
return (
<div>
<div className="flex items-baseline justify-between">
<div className="flex items-center justify-between gap-3">
<label className="text-sm font-medium">{label}</label>
<span className="text-2xl font-bold tabular-nums">{value}{suffix && <span className="text-sm text-slate-500">{suffix}</span>}</span>
<div className="flex items-center gap-2">
<button type="button" onClick={dec} disabled={value <= min} aria-label={`Уменьшить: ${label}`} className={btn}>−</button>
<span className="text-2xl font-bold tabular-nums w-10 text-center">{value}{suffix && <span className="text-sm text-slate-500">{suffix}</span>}</span>
<button type="button" onClick={inc} disabled={value >= max} aria-label={`Увеличить: ${label}`} className={btn}>+</button>
</div>
</div>
<input
type="range" min={min} max={max} value={value}
onChange={(e) => onChange(Number(e.target.value))}
className="w-full mt-2 accent-[#00B207]"
className="fm-range w-full mt-2 accent-[#00B207]"
style={{ touchAction: 'pan-y' }}
/>
<div className="flex justify-between text-xs text-slate-400 mt-1"><span>{min}</span><span>{max}</span></div>
</div>
@ -38,10 +46,11 @@ export default function BusinessTariffBuilder() {
const signupHref = `/signup?plan=business&shops=${shops}&cashes=${cashes}&warehouses=${warehouses}&employees=${employees}`
return (
<div className="rounded-xl border-2 border-brand bg-white p-6">
<div className="relative rounded-xl border-2 border-brand bg-white p-6">
<span className="absolute -top-3 left-1/2 -translate-x-1/2 bg-brand text-white text-xs px-2 py-0.5 rounded-full">Популярный</span>
<div className="flex items-baseline justify-between mb-6">
<div>
<h3 class="text-lg font-bold">Конструктор «Бизнес»</h3>
<h3 className="text-lg font-bold">Конструктор «Бизнес»</h3>
<p className="text-xs text-slate-500 mt-0.5">База 10 000 ₸ + ваши параметры</p>
</div>
<div className="text-right">
@ -63,6 +72,7 @@ export default function BusinessTariffBuilder() {
<a href={signupHref} className="block text-center mt-6 px-4 py-3 bg-brand text-white rounded-md font-semibold hover:bg-[#009305]">
Выбрать конфигурацию · {fmt.format(total)} ₸/мес
</a>
<p className="text-xs text-center text-slate-500 mt-2">90 дней бесплатно, без карты</p>
</div>
)
}

View file

@ -25,7 +25,7 @@ import Logo from './Logo.astro'
<ul class="space-y-1.5">
<li><a href="/kb" class="block py-2 text-slate-700 hover:text-brand">База знаний</a></li>
<li><a href="/blog" class="block py-2 text-slate-700 hover:text-brand">Блог</a></li>
<li><a href="/status" class="block py-2 text-slate-700 hover:text-brand">Status</a></li>
<li><a href="/status" class="block py-2 text-slate-700 hover:text-brand">Статус</a></li>
<li><a href="/changelog" class="block py-2 text-slate-700 hover:text-brand">Changelog</a></li>
</ul>
</div>

View file

@ -6,27 +6,33 @@ const nav = [
{ href: '/pos', label: 'Касса' },
{ href: '/for-grocery', label: 'Для продуктовых' },
{ href: '/import', label: 'Импорт' },
{ href: '/integrations', label: 'Интеграции' },
{ href: '/pricing', label: 'Тарифы' },
{ href: '/kb', label: 'База знаний' },
{ href: '/blog', label: 'Блог' },
]
// Вторичные ссылки — внизу мобильной панели (в десктоп-навигацию не выносим).
const secondary = [
{ href: '/status', label: 'Статус' },
{ href: '/changelog', label: 'Что нового' },
]
---
<header class="sticky top-0 z-40 bg-white/95 backdrop-blur border-b border-slate-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between gap-4">
<a href="/" class="flex items-center gap-2"><Logo /></a>
<nav class="hidden lg:flex items-center gap-1 text-sm">
<nav class="hidden xl:flex items-center gap-1 text-sm">
{nav.map((i) => <a href={i.href} class="px-3 py-2 hover:text-brand transition">{i.label}</a>)}
</nav>
<div class="flex items-center gap-2">
<a href={`${APP_URL}/login`} class="px-3 min-h-[44px] flex items-center text-sm text-slate-700 hover:text-brand">Войти</a>
<a href="/signup" class="px-3 min-h-[44px] flex items-center text-sm font-medium bg-brand text-white rounded-md hover:bg-brand-hover">Начать бесплатно</a>
<!-- Гамбургер: только на планшете/мобильном (десктоп-меню скрыто < lg) -->
<div class="flex items-center gap-2 shrink-0">
<a href={`${APP_URL}/login`} class="hidden sm:flex px-3 min-h-[44px] items-center text-sm text-slate-700 hover:text-brand whitespace-nowrap">Войти</a>
<a href="/signup" class="px-3 min-h-[44px] flex items-center text-sm font-medium bg-brand text-white rounded-md hover:bg-brand-hover whitespace-nowrap">Начать бесплатно</a>
<!-- Гамбургер: планшет/мобильный и средний десктоп (полное меню — от xl) -->
<button
id="mobile-menu-btn"
type="button"
class="lg:hidden inline-flex items-center justify-center w-11 h-11 -mr-1 rounded-md text-slate-700 hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-brand"
class="xl:hidden inline-flex items-center justify-center w-11 h-11 -mr-1 rounded-md text-slate-700 hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-brand"
aria-label="Меню"
aria-controls="mobile-menu"
aria-expanded="false"
@ -45,6 +51,9 @@ const nav = [
<a href={`${APP_URL}/login`} class="flex items-center justify-center min-h-[44px] px-3 rounded-md text-sm font-medium border border-slate-300 text-slate-700 hover:bg-slate-50">Войти</a>
<a href="/signup" class="flex items-center justify-center min-h-[44px] px-3 rounded-md text-sm font-medium bg-brand text-white hover:bg-brand-hover">Начать бесплатно</a>
</div>
<div class="mt-2 pt-2 border-t border-slate-100 flex flex-wrap gap-x-4">
{secondary.map((i) => <a href={i.href} class="flex items-center min-h-[44px] px-3 rounded-md text-sm text-slate-500 hover:text-brand">{i.label}</a>)}
</div>
</div>
</nav>
</header>
@ -55,16 +64,32 @@ const nav = [
const iconOpen = document.getElementById('mobile-menu-icon-open')
const iconClose = document.getElementById('mobile-menu-icon-close')
if (btn && menu) {
const setOpen = (open: boolean) => {
// Пока панель открыта — в истории висит наша запись, чтобы системная кнопка
// «Назад» (Android) закрывала меню, а не уводила со страницы.
let pushed = false
const setOpen = (open: boolean, fromPop = false) => {
menu.classList.toggle('hidden', !open)
btn.setAttribute('aria-expanded', String(open))
iconOpen?.classList.toggle('hidden', open)
iconClose?.classList.toggle('hidden', !open)
if (open) {
if (!pushed) { history.pushState({ menu: true }, ''); pushed = true }
} else if (pushed && !fromPop) {
// закрыли сами (Esc/крестик/тап/resize) — снимаем запись из истории
pushed = false
history.back()
} else {
pushed = false
}
}
btn.addEventListener('click', (e) => {
e.stopPropagation()
setOpen(menu.classList.contains('hidden'))
})
// системная кнопка «Назад» / popstate — закрыть панель
window.addEventListener('popstate', () => {
if (!menu.classList.contains('hidden')) setOpen(false, true)
})
// закрытие по тапу вне меню
document.addEventListener('click', (e) => {
if (menu.classList.contains('hidden')) return
@ -76,6 +101,6 @@ const nav = [
if (e.key === 'Escape' && !menu.classList.contains('hidden')) { setOpen(false); (btn as HTMLButtonElement).focus() }
})
// при переходе к десктопной ширине — сбросить состояние
window.matchMedia('(min-width: 1024px)').addEventListener('change', (m) => { if (m.matches) setOpen(false) })
window.matchMedia('(min-width: 1280px)').addEventListener('change', (m) => { if (m.matches && !menu.classList.contains('hidden')) setOpen(false) })
}
</script>

View file

@ -24,19 +24,3 @@ const fmt = post.data.date.toLocaleDateString('ru-KZ', { day: '2-digit', month:
<div class="prose-md mt-8"><Content /></div>
</article>
</BaseLayout>
<style is:global>
.prose-md { font-size: 16px; line-height: 1.75; color: #1e293b; }
.prose-md h2 { font-size: 1.5rem; font-weight: 700; margin-top: 2rem; margin-bottom: 0.75rem; }
.prose-md h3 { font-size: 1.125rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; }
.prose-md p { margin: 1rem 0; }
.prose-md ul, .prose-md ol { margin: 1rem 0 1rem 1.5rem; }
.prose-md li { margin: 0.35rem 0; }
.prose-md a { color: #00B207; text-decoration: underline; }
.prose-md blockquote { border-left: 3px solid #00B207; background: #E8F5E9; padding: 0.75rem 1rem; margin: 1.25rem 0; font-style: italic; color: #1e293b; }
.prose-md code { background: #f1f5f9; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875em; }
.prose-md hr { margin: 2rem 0; border: 0; border-top: 1px solid #e2e8f0; }
.prose-md strong { font-weight: 600; }
.prose-md table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.95rem; }
.prose-md th, .prose-md td { border: 1px solid #e2e8f0; padding: 0.5rem 0.75rem; text-align: left; }
.prose-md th { background: #f8fafc; font-weight: 600; }
</style>

View file

@ -22,13 +22,12 @@ import BusinessTariffBuilder from '@/components/BusinessTariffBuilder.tsx'
<li>✓ 2 сотрудника</li>
<li>✓ Без лимита товаров</li>
<li>✓ Касса с поддержкой торговых весов</li>
<li>✓ Импорт из других систем</li>
<li>✓ Перенос каталога из CSV и МойСклад</li>
<li>✓ Перенос каталога из файлов Excel/CSV</li>
</ul>
<a href="/signup?plan=start" class="block text-center mt-6 px-4 py-2.5 bg-brand text-white rounded-md font-semibold">Начать</a>
</div>
<div class="lg:row-span-2">
<div id="configurator" class="lg:row-span-2 scroll-mt-20">
<BusinessTariffBuilder client:load />
</div>

View file

@ -10,3 +10,36 @@
html { scroll-behavior: smooth; }
body { background: #fff; color: #0f172a; }
/* Эмодзи-фолбэк: чтобы иконки-эмодзи не рисовались квадратами там, где нет цветного
emoji-шрифта (отдельные окружения/симуляторы). */
:root { --emoji: 'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji','Twemoji Mozilla',sans-serif; }
body { font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji', sans-serif; }
/* Типографика markdown-контента (База знаний и блог) — ОДИН источник на весь сайт
(раньше стили были только на странице блога → /kb/* рисовались плоским текстом). */
.prose-md { font-size: 16px; line-height: 1.75; color: #1e293b; }
.prose-md h2 { font-size: 1.5rem; font-weight: 700; margin-top: 2rem; margin-bottom: 0.75rem; }
.prose-md h3 { font-size: 1.125rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; }
.prose-md p { margin: 1rem 0; }
.prose-md ul { list-style: disc; margin: 1rem 0 1rem 1.5rem; }
.prose-md ol { list-style: decimal; margin: 1rem 0 1rem 1.5rem; }
.prose-md li { margin: 0.35rem 0; }
.prose-md a { color: #00B207; text-decoration: underline; }
.prose-md blockquote { border-left: 3px solid #00B207; background: #E8F5E9; padding: 0.75rem 1rem; margin: 1.25rem 0; font-style: italic; color: #1e293b; }
.prose-md code { background: #f1f5f9; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875em; }
.prose-md hr { margin: 2rem 0; border: 0; border-top: 1px solid #e2e8f0; }
.prose-md strong { font-weight: 600; }
.prose-md table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.95rem; }
.prose-md th, .prose-md td { border: 1px solid #e2e8f0; padding: 0.5rem 0.75rem; text-align: left; }
.prose-md th { background: #f8fafc; font-weight: 600; }
/* Увеличенная зона нажатия для инлайн-ссылок (мобильный) без сдвига вёрстки. */
.prose-md a { padding-block: 3px; margin-block: -3px; }
/* Ползунки конструктора «Бизнес»: крупная ручка (удобно пальцем) + вертикальный
скролл страницы не двигает значение (touch-action задан в компоненте). */
.fm-range { height: 2.25rem; background: transparent; cursor: pointer; }
.fm-range::-webkit-slider-runnable-track { height: 6px; border-radius: 9999px; background: #e2e8f0; }
.fm-range::-moz-range-track { height: 6px; border-radius: 9999px; background: #e2e8f0; }
.fm-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 28px; height: 28px; margin-top: -11px; border-radius: 9999px; background: #00B207; border: 3px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3); }
.fm-range::-moz-range-thumb { width: 28px; height: 28px; border-radius: 9999px; background: #00B207; border: 3px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3); }