Files
test/app/app.vue
2026-02-10 15:56:17 +07:00

1114 lines
46 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div data-theme="silk" class="min-h-screen bg-mesh text-neutral">
<canvas id="canvas-basic" class="gradient-canvas" aria-hidden="true"></canvas>
<div class="relative z-10">
<div
class="pointer-events-none absolute -top-24 left-1/2 h-72 w-[1100px] -translate-x-1/2 rounded-full bg-[radial-gradient(circle_at_center,rgba(67,56,118,0.25),transparent_70%)] blur-3xl"
></div>
<main class="relative px-6 pb-16 pt-10 md:px-12 lg:px-20">
<div class="grid gap-8 lg:grid-cols-[220px_1fr]">
<aside class="hidden lg:block">
<div class="glass-panel sticky top-10 rounded-3xl border border-base-300 bg-base-100/90 p-5 shadow-soft">
<div class="text-xs uppercase tracking-[0.2em] text-neutral/50">Навигация</div>
<nav class="mt-4 space-y-2 text-sm">
<a class="nav-link block rounded-2xl px-3 py-2 text-neutral/70 border border-transparent" href="#context">Контекст</a>
<a class="nav-link block rounded-2xl px-3 py-2 text-neutral/70 border border-transparent" href="#problem">Проблемы</a>
<a class="nav-link block rounded-2xl px-3 py-2 text-neutral/70 border border-transparent" href="#core">Core-фича</a>
<a class="nav-link block rounded-2xl px-3 py-2 text-neutral/70 border border-transparent" href="#flow">Как работает</a>
<a class="nav-link block rounded-2xl px-3 py-2 text-neutral/70 border border-transparent" href="#value">Value Proposition</a>
<a class="nav-link block rounded-2xl px-3 py-2 text-neutral/70 border border-transparent" href="#prototype">Прототип</a>
<a class="nav-link block rounded-2xl px-3 py-2 text-neutral/70 border border-transparent" href="#monetization">Монетизация</a>
<a class="nav-link block rounded-2xl px-3 py-2 text-neutral/70 border border-transparent" href="#metrics">Метрики</a>
<a class="nav-link block rounded-2xl px-3 py-2 text-neutral/70 border border-transparent" href="#roadmap">Roadmap</a>
<a class="nav-link block rounded-2xl px-3 py-2 text-neutral/70 border border-transparent" href="#final">Итог</a>
</nav>
</div>
</aside>
<div class="space-y-10">
<section id="context" class="slide">
<div class="slide-top">Контекст</div>
<div class="slide-head">
<h2 class="slide-title">Ремонт редкий и дорогой опыт</h2>
<p class="slide-preamble">
Большинство людей проходят ремонт 12 раза в жизни. Это эмоционально тяжелый процесс с
высокими ставками, где нет навыка управления и нет стандарта качества. Поэтому каждый
проект начинается заново: ожидания не совпадают, правила не зафиксированы, а неопределенность
быстро превращается в конфликт. Заказчик хочет контроля и понятных правил, исполнитель ясных
критериев и гарантии оплаты.
</p>
</div>
<div class="slide-main"></div>
</section>
<section id="problem" class="slide">
<div class="slide-top">Проблемы</div>
<div class="slide-head">
<h2 class="slide-title">Ключевые проблемы</h2>
<p class="slide-preamble">
Мы провели интервью с заказчиками и мастерами и увидели, что проблемы повторяются из проекта
в проект. На практике это всегда упирается в несколько узлов, которые ломают доверие и сроки.
Ниже три ключевых вывода.
</p>
</div>
<div class="slide-main">
<div class="grid gap-6 md:grid-cols-3 md:items-end">
<div class="glass-card rounded-3xl bg-base-100/90 p-6 shadow-soft md:translate-y-8">
<h3 class="font-display text-2xl">Ожидания и реальность расходятся</h3>
<p class="mt-3 text-sm text-neutral/70">
Нет общего понимания результата: стороны по-разному трактуют «готово» и «качественно».
</p>
<p class="mt-3 text-xs text-neutral/60">Заказчик: не уверен, что «сделано правильно». Исполнитель: не понимает, как это доказать.</p>
</div>
<div class="glass-card rounded-3xl bg-base-100/90 p-6 shadow-soft md:translate-y-14">
<h3 class="font-display text-2xl">Критерии не зафиксированы</h3>
<p class="mt-3 text-sm text-neutral/70">
Нет списка требований, допусков и контрольных точек спор легко уходит в эмоции.
</p>
<p class="mt-3 text-xs text-neutral/60">Заказчик: нет прозрачной проверки. Исполнитель: нет единого регламента.</p>
</div>
<div class="glass-card rounded-3xl bg-base-100/90 p-6 shadow-soft md:translate-y-4">
<h3 class="font-display text-2xl">Финансовый риск у обеих сторон</h3>
<p class="mt-3 text-sm text-neutral/70">
Платежи завязаны на доверии, а не на факте качества обе стороны боятся потерь.
</p>
<p class="mt-3 text-xs text-neutral/60">Заказчик: боится переплатить. Исполнитель: боится не получить деньги.</p>
</div>
</div>
</div>
</section>
<section id="core" class="slide">
<div class="slide-top">Core-фича</div>
<div class="slide-head">
<h2 class="slide-title">Ноу-хау: приемка как сервис</h2>
<p class="slide-preamble">
Проанализировав проблемы, мы видим ключевую боль именно в приемке. Когда заранее понятно,
что такое хорошо и что делать дальше в каждом исходе, исчезает источник конфликта. Это и есть
наша killer-feature: критерии фиксируются до старта, а оценка становится независимой.
</p>
</div>
<div class="slide-main">
<div class="glass-panel rounded-3xl border border-base-300 bg-base-100/90 p-6">
<div class="text-xs font-semibold text-neutral/60">Пример регламентной приемки</div>
<div class="mt-4 grid grid-cols-[1.1fr_1.3fr_0.9fr_1fr] gap-3 text-sm">
<div class="rounded-2xl bg-base-200 px-4 py-3 font-semibold text-neutral/70">Работа</div>
<div class="rounded-2xl bg-base-200 px-4 py-3 font-semibold text-neutral/70">Измерено</div>
<div class="rounded-2xl bg-base-200 px-4 py-3 font-semibold text-neutral/70 text-center">Оценка</div>
<div class="rounded-2xl bg-base-200 px-4 py-3 font-semibold text-neutral/70">Решение</div>
<div class="rounded-2xl bg-white px-4 py-3">Укладка плитки</div>
<div class="rounded-2xl bg-white px-4 py-3">Отклонение 1 мм при норме 2 мм</div>
<div class="rounded-2xl bg-success/15 px-4 py-3 text-center text-success">Ок</div>
<div class="rounded-2xl bg-white px-4 py-3">Оплата 100%</div>
<div class="rounded-2xl bg-white px-4 py-3">Электронный уровень</div>
<div class="rounded-2xl bg-white px-4 py-3">Отклонение 2° при норме 1°</div>
<div class="rounded-2xl bg-warning/15 px-4 py-3 text-center text-warning">На грани</div>
<div class="rounded-2xl bg-white px-4 py-3">Удержать 5%</div>
<div class="rounded-2xl bg-white px-4 py-3">Швы</div>
<div class="rounded-2xl bg-white px-4 py-3">Разбег 2 мм при норме 1 мм</div>
<div class="rounded-2xl bg-error/15 px-4 py-3 text-center text-error">Не ок</div>
<div class="rounded-2xl bg-white px-4 py-3">Переделка участка</div>
</div>
</div>
</div>
</section>
<section id="flow" class="slide">
<div class="slide-top">Как работает</div>
<div class="slide-head">
<h2 class="slide-title">Простой сценарий в три шага</h2>
<p class="slide-preamble">
Мы переводим идею в простой процесс: три шага, которые последовательно ведут клиента от
неопределенности к прозрачной приемке и защищенной оплате для обеих сторон.
</p>
</div>
<div class="slide-main">
<div class="grid gap-6 md:grid-cols-3">
<div class="glass-card rounded-3xl bg-base-100/90 p-6 shadow-soft">
<div class="text-xs font-semibold text-neutral/60">Шаг 1</div>
<h3 class="mt-2 font-display text-2xl">Вход и расчет</h3>
<p class="mt-3 text-sm text-neutral/70">
Заказчик загружает объект, система сразу считает смету и формирует план.
</p>
</div>
<div class="glass-card rounded-3xl bg-base-100/90 p-6 shadow-soft">
<div class="text-xs font-semibold text-neutral/60">Шаг 2</div>
<h3 class="mt-2 font-display text-2xl">Регламент и работа</h3>
<p class="mt-3 text-sm text-neutral/70">
Этапы и критерии фиксируются, исполнитель ведет работы по чек-листам.
</p>
</div>
<div class="glass-card rounded-3xl bg-base-100/90 p-6 shadow-soft">
<div class="text-xs font-semibold text-neutral/60">Шаг 3</div>
<h3 class="mt-2 font-display text-2xl">Независимая проверка</h3>
<p class="mt-3 text-sm text-neutral/70">
Инспектор подтверждает качество, сервис переводит эскроу исполнителю по результату.
</p>
</div>
</div>
</div>
</section>
<section id="value" class="slide">
<div class="slide-top">Value Proposition</div>
<div class="slide-head">
<h2 class="slide-title">Что получает каждая сторона</h2>
<p class="slide-preamble">
Итог сценария должен быть очевиден для обеих ролей. Мы фиксируем ценность отдельно для
заказчика и для исполнителя, чтобы не было перекоса и спора за «справедливость».
</p>
</div>
<div class="slide-main">
<div class="grid gap-6 md:grid-cols-2">
<div class="glass-card rounded-3xl bg-base-100/90 p-6 shadow-soft">
<h3 class="font-display text-2xl">Заказчик</h3>
<p class="mt-3 text-sm text-neutral/70">
Прозрачная смета, понятные критерии качества и независимая приемка. Деньги защищены
эскроу, а результат подтвержден экспертом.
</p>
</div>
<div class="glass-card rounded-3xl bg-base-100/90 p-6 shadow-soft">
<h3 class="font-display text-2xl">Исполнитель</h3>
<p class="mt-3 text-sm text-neutral/70">
Четкий регламент работ, меньше спорных ситуаций и гарантированная оплата после проверки.
Проще сдавать этапы и выстраивать доверие.
</p>
</div>
</div>
</div>
</section>
<section id="prototype" class="slide">
<div class="slide-top">Прототип</div>
<div class="slide-head">
<h2 class="slide-title">Один экран один смысл</h2>
<p class="slide-preamble">
Посмотрите прототип: мы подготовили сценарий, который показывает, как пользователь проходит
путь от объекта до понятной сметы, этапов и уведомлений. Окунитесь в то, как это может выглядеть.
</p>
</div>
<div class="slide-main">
<div class="relative flex justify-center">
<div class="iphone-frame shadow-glow">
<span class="iphone-button iphone-button--mute" aria-hidden="true"></span>
<span class="iphone-button iphone-button--vol-up" aria-hidden="true"></span>
<span class="iphone-button iphone-button--vol-down" aria-hidden="true"></span>
<span class="iphone-button iphone-button--power" aria-hidden="true"></span>
<div class="iphone-screen">
<div class="iphone-notch" aria-hidden="true"></div>
<iframe
class="iphone-iframe"
title="Прототип пользовательского пути"
:srcdoc="prototypeSrcdoc"
sandbox="allow-scripts allow-same-origin"
></iframe>
</div>
</div>
</div>
</div>
</section>
<section id="monetization" class="slide">
<div class="slide-top">Монетизация</div>
<div class="slide-head">
<h2 class="slide-title">Прозрачные источники выручки</h2>
<p class="slide-preamble">
Мы фиксируем этапы, сроки и материалы значит, точно знаем, где создается ценность. Это
делает монетизацию понятной для обеих сторон и логично вытекающей из процесса.
</p>
</div>
<div class="slide-main">
<div class="glass-panel relative overflow-hidden rounded-3xl border border-base-300 bg-base-100/90 p-8">
<div class="pointer-events-none absolute -left-16 top-10 h-36 w-36 rounded-full bg-primary/15 blur-3xl"></div>
<div class="pointer-events-none absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-secondary/20 blur-3xl"></div>
<div class="relative grid gap-6 md:grid-cols-3">
<div class="glass-card rounded-3xl bg-white/90 p-6 shadow-soft">
<div class="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/15 text-primary">
<svg viewBox="0 0 24 24" class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.6">
<path d="M12 3v18M7 7h5a3 3 0 1 1 0 6H9a3 3 0 1 0 0 6h5" />
</svg>
</div>
<h3 class="font-display text-2xl">Комиссия эскроу</h3>
<p class="mt-3 text-sm text-neutral/70">Процент от подтвержденного этапа, удерживается сервисом.</p>
</div>
<div class="glass-card rounded-3xl bg-white/90 p-6 shadow-soft">
<div class="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/15 text-primary">
<svg viewBox="0 0 24 24" class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.6">
<path d="M4 20h16M6 20l2-10h8l2 10M9 10V6a3 3 0 0 1 6 0v4" />
</svg>
</div>
<h3 class="font-display text-2xl">Инспекция</h3>
<p class="mt-3 text-sm text-neutral/70">Плата за выезд и экспертизу, инициирует заказчик.</p>
</div>
<div class="glass-card rounded-3xl bg-white/90 p-6 shadow-soft">
<div class="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/15 text-primary">
<svg viewBox="0 0 24 24" class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.6">
<path d="M4 7h16l-2 10H6L4 7z" />
<path d="M8 7V5a4 4 0 0 1 8 0v2" />
</svg>
</div>
<h3 class="font-display text-2xl">Материалы</h3>
<p class="mt-3 text-sm text-neutral/70">За материалы платит заказчик, сервис получает маржу.</p>
</div>
</div>
</div>
</div>
</section>
<section id="metrics" class="slide">
<div class="slide-top">Метрики</div>
<div class="slide-head">
<h2 class="slide-title">Проверяем, что продукт работает</h2>
<p class="slide-preamble">
Метрики должны показать, что сервис снижает конфликт и ускоряет приемку. Мы отслеживаем
ключевые сигналы качества, скорости и точности.
</p>
</div>
<div class="slide-main">
<div class="grid gap-6 md:grid-cols-[1fr_1fr]">
<div>
<h3 class="font-display text-2xl">North Star Metric</h3>
<p class="mt-3 text-sm text-neutral/70">Доля этапов, принятых с первого раза.</p>
<p class="mt-3 text-sm text-neutral/70">
Влияют: точность автосметы, скорость приемки, доля зеленых оценок.
</p>
</div>
<div class="grid gap-4">
<div class="glass-card rounded-3xl border border-base-300 bg-base-100/90 p-6">
<div class="text-xs font-semibold text-neutral/60">Качество</div>
<div class="mt-2 font-display text-xl">Доля зеленых приемок</div>
</div>
<div class="glass-card rounded-3xl border border-base-300 bg-base-100/90 p-6">
<div class="text-xs font-semibold text-neutral/60">Скорость</div>
<div class="mt-2 font-display text-xl">Среднее время приемки этапа</div>
</div>
<div class="glass-card rounded-3xl border border-base-300 bg-base-100/90 p-6">
<div class="text-xs font-semibold text-neutral/60">Точность</div>
<div class="mt-2 font-display text-xl">Отклонение сметы от факта</div>
</div>
</div>
</div>
</div>
</section>
<section id="roadmap" class="slide">
<div class="slide-top">Roadmap</div>
<div class="slide-head">
<h2 class="slide-title">Валидация и масштабирование</h2>
<p class="slide-preamble">
В фокусе не разработка, а проверка гипотез и запуск в реальных процессах. Сначала пилот
и валидируемые результаты, затем подключение партнеров и масштабирование.
</p>
</div>
<div class="slide-main">
<div class="glass-panel rounded-3xl border border-base-300 bg-base-100/90 p-6">
<div class="grid gap-4 text-sm">
<div class="grid grid-cols-6 gap-2 text-xs text-neutral/50">
<div>М1</div>
<div>М2</div>
<div>М3</div>
<div>М4</div>
<div>М5</div>
<div>М6</div>
</div>
<div class="grid grid-cols-[160px_1fr] items-center gap-4">
<div class="text-neutral/70">Пилот и гипотезы</div>
<div class="grid grid-cols-6 gap-2">
<div class="col-span-2 rounded-full bg-primary/80 py-2 text-center text-xs text-primary-content">М1М2</div>
<div class="col-span-4 rounded-full bg-base-200 py-2"></div>
</div>
</div>
<div class="grid grid-cols-[160px_1fr] items-center gap-4">
<div class="text-neutral/70">Встраивание в процессы</div>
<div class="grid grid-cols-6 gap-2">
<div class="col-span-2 rounded-full bg-base-200 py-2"></div>
<div class="col-span-2 rounded-full bg-primary/80 py-2 text-center text-xs text-primary-content">М3М4</div>
<div class="col-span-2 rounded-full bg-base-200 py-2"></div>
</div>
</div>
<div class="grid grid-cols-[160px_1fr] items-center gap-4">
<div class="text-neutral/70">Масштабирование</div>
<div class="grid grid-cols-6 gap-2">
<div class="col-span-4 rounded-full bg-base-200 py-2"></div>
<div class="col-span-2 rounded-full bg-primary/80 py-2 text-center text-xs text-primary-content">М5М6</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="final" class="slide">
<div class="slide-main items-center justify-center text-center">
<div class="flex flex-col items-center gap-4">
<img src="/avatar.jpg" alt="Руслан Бакиев" class="h-28 w-28 rounded-full object-cover ring-2 ring-base-300" />
<div class="font-display text-3xl text-neutral">Спасибо за внимание</div>
<div class="text-neutral/70">
<div class="text-lg text-neutral">Руслан Бакиев</div>
<div class="text-sm">Телеграм: veikab</div>
</div>
</div>
</div>
</section>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue'
import Granim from 'granim'
useHead({
title: 'Контроль ремонта — презентация',
meta: [
{ name: 'description', content: 'Презентация продукта: смета, приемка, эскроу и методология ремонта.' }
]
})
const prototypeSrcdoc = `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Прототип</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&display=swap');
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
height: 100%;
font-family: 'Manrope', system-ui, sans-serif;
color: #171717;
background: #f4f1eb;
}
.app {
height: 100%;
display: flex;
flex-direction: column;
gap: 12px;
padding: 34px 16px 18px;
background: linear-gradient(160deg, rgba(255, 255, 255, 0.95), rgba(243, 239, 232, 0.92));
}
.status {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #2d2d2d;
}
.pill {
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
background: rgba(26, 77, 122, 0.12);
color: #1a4d7a;
font-weight: 600;
}
.icons {
display: flex;
align-items: center;
gap: 6px;
}
.dot {
width: 6px;
height: 6px;
border-radius: 999px;
background: #1a4d7a;
}
.progress {
position: relative;
height: 6px;
background: rgba(26, 77, 122, 0.12);
border-radius: 999px;
overflow: hidden;
}
.progress__bar {
position: absolute;
inset: 0;
width: 10%;
background: linear-gradient(90deg, #1a4d7a, #3f7fb0);
border-radius: inherit;
transition: width 0.35s ease;
}
.card {
background: rgba(255, 255, 255, 0.92);
border-radius: 18px;
border: 1px solid rgba(23, 23, 23, 0.08);
padding: 14px;
box-shadow: 0 12px 28px rgba(23, 23, 23, 0.08);
}
.card.main {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
}
.eyebrow {
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: rgba(23, 23, 23, 0.5);
}
.title {
font-size: 20px;
font-weight: 700;
}
.subtitle {
font-size: 12px;
color: rgba(23, 23, 23, 0.64);
margin: 0;
}
.list {
display: grid;
gap: 8px;
margin-top: 4px;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
border-radius: 12px;
background: #f0ece6;
font-size: 12px;
}
.item strong {
font-weight: 600;
}
.tag {
padding: 2px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 600;
}
.tag.success {
background: rgba(31, 129, 84, 0.14);
color: #1f8154;
}
.tag.pending {
background: rgba(211, 135, 32, 0.15);
color: #b7741c;
}
.tag.info {
background: rgba(26, 77, 122, 0.14);
color: #1a4d7a;
}
.info-card {
font-size: 12px;
color: rgba(23, 23, 23, 0.7);
line-height: 1.4;
}
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.actions .btn {
border: none;
border-radius: 14px;
padding: 10px 12px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
}
.actions .btn:active {
transform: scale(0.98);
}
.btn.primary {
background: #1a4d7a;
color: #fff;
box-shadow: 0 10px 20px rgba(26, 77, 122, 0.3);
grid-column: span 2;
}
.btn.secondary {
background: rgba(26, 77, 122, 0.12);
color: #1a4d7a;
}
.btn.ghost {
background: rgba(23, 23, 23, 0.06);
color: rgba(23, 23, 23, 0.75);
}
.btn[disabled] {
opacity: 0.45;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.cta-row {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.cta {
padding: 6px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
background: #1a4d7a;
color: #fff;
}
.cta.light {
background: rgba(26, 77, 122, 0.12);
color: #1a4d7a;
}
</style>
</head>
<body>
<div class="app">
<div class="status">
<div>9:41</div>
<div class="pill">Контроль ремонта</div>
<div class="icons">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
<div class="progress">
<div class="progress__bar" id="progressBar"></div>
</div>
<div class="card main">
<div class="eyebrow" id="stepLabel"></div>
<div class="title" id="stepTitle"></div>
<p class="subtitle" id="stepSubtitle"></p>
<div id="stepContent"></div>
</div>
<div class="card info-card" id="helperCard"></div>
<div class="actions">
<button class="btn ghost" data-action="back">Назад</button>
<button class="btn secondary" data-action="primary">Добавить</button>
<button class="btn primary" data-action="next">Далее</button>
</div>
</div>
<script>
(function () {
var state = {
step: 0,
projectCreated: false,
addressAdded: false,
mediaAdded: false,
quoteApproved: false,
stageStarted: false,
accepted: false
};
function resetState() {
state.step = 0;
state.projectCreated = false;
state.addressAdded = false;
state.mediaAdded = false;
state.quoteApproved = false;
state.stageStarted = false;
state.accepted = false;
}
var steps = [
{
label: 'Шаг 1 из 6',
title: 'Старт проекта',
subtitle: 'Зафиксируйте объект, чтобы запустить сценарий.',
primaryLabel: 'Создать проект',
onPrimary: function () {
state.projectCreated = true;
},
ready: function () {
return state.projectCreated;
},
content: function () {
var status = state.projectCreated ? 'Проект создан' : 'Проект не создан';
var tagClass = state.projectCreated ? 'success' : 'pending';
return (
'<div class="list">' +
'<div class="item"><strong>Объект</strong><span class="tag ' +
tagClass +
'">' +
status +
'</span></div>' +
'<div class="item"><strong>План приемки</strong><span class="tag info">В подготовке</span></div>' +
'</div>'
);
},
helper: function () {
return state.projectCreated
? 'Проект создан. Теперь добавьте адрес и параметры объекта, чтобы расчет стал точным.'
: 'Нажмите «Создать проект», чтобы зафиксировать старт и открыть следующие шаги.';
}
},
{
label: 'Шаг 2 из 6',
title: 'Добавление объекта',
subtitle: 'Адрес и параметры нужны для точной сметы.',
primaryLabel: 'Добавить адрес',
onPrimary: function () {
state.addressAdded = true;
},
ready: function () {
return state.addressAdded;
},
content: function () {
var address = state.addressAdded ? 'Москва, ул. Правды, 12' : 'Адрес не указан';
var tagClass = state.addressAdded ? 'success' : 'pending';
return (
'<div class="list">' +
'<div class="item"><strong>Адрес</strong><span class="tag ' +
tagClass +
'">' +
address +
'</span></div>' +
'<div class="item"><strong>Метраж</strong><span class="tag info">68 м²</span></div>' +
'</div>'
);
},
helper: function () {
return state.addressAdded
? 'Адрес сохранен. Переходим к медиа — это помогает зафиксировать стартовое состояние.'
: 'Добавьте адрес, чтобы расчет сметы учитывал реальный объект.';
}
},
{
label: 'Шаг 3 из 6',
title: 'Фото и видео',
subtitle: 'Фиксируем стартовое состояние объекта.',
primaryLabel: 'Добавить медиа',
onPrimary: function () {
state.mediaAdded = true;
},
ready: function () {
return state.mediaAdded;
},
content: function () {
var text = state.mediaAdded ? '12 фото, 2 видео' : 'Медиа не загружены';
var tagClass = state.mediaAdded ? 'success' : 'pending';
return (
'<div class="list">' +
'<div class="item"><strong>Загрузка</strong><span class="tag ' +
tagClass +
'">' +
text +
'</span></div>' +
'<div class="item"><strong>Комментарий</strong><span class="tag info">Черновая стяжка</span></div>' +
'</div>'
);
},
helper: function () {
return state.mediaAdded
? 'Медиа загружены. Система сформирует смету и этапы на основании материалов.'
: 'Добавьте фото и видео, чтобы у обеих сторон была единая точка отсчета.';
}
},
{
label: 'Шаг 4 из 6',
title: 'Смета и этапы',
subtitle: 'Проверьте расчет и зафиксируйте этапы.',
primaryLabel: 'Согласовать смету',
onPrimary: function () {
state.quoteApproved = true;
},
ready: function () {
return state.quoteApproved;
},
content: function () {
var tagClass = state.quoteApproved ? 'success' : 'pending';
var status = state.quoteApproved ? 'Согласовано' : 'На согласовании';
return (
'<div class="list">' +
'<div class="item"><strong>Смета</strong><span class="tag ' +
tagClass +
'">' +
status +
'</span></div>' +
'<div class="item"><strong>Этапы</strong><span class="tag info">6 этапов</span></div>' +
'</div>'
);
},
helper: function () {
return state.quoteApproved
? 'Смета согласована. Можно запускать работы по первому этапу.'
: 'Согласуйте смету, чтобы зафиксировать финансовую модель и регламент.';
}
},
{
label: 'Шаг 5 из 6',
title: 'Старт этапа',
subtitle: 'Исполнитель следует чек-листу и фиксирует ход.',
primaryLabel: 'Запустить этап 1',
onPrimary: function () {
state.stageStarted = true;
},
ready: function () {
return state.stageStarted;
},
content: function () {
var tagClass = state.stageStarted ? 'success' : 'pending';
var status = state.stageStarted ? 'В работе' : 'Не запущен';
return (
'<div class="list">' +
'<div class="item"><strong>Этап 1</strong><span class="tag ' +
tagClass +
'">' +
status +
'</span></div>' +
'<div class="item"><strong>Чек-лист</strong><span class="tag info">12 пунктов</span></div>' +
'</div>'
);
},
helper: function () {
return state.stageStarted
? 'Этап запущен. Система подготовит уведомление о приемке.'
: 'Запустите этап, чтобы начать работу и фиксировать прогресс.';
}
},
{
label: 'Шаг 6 из 6',
title: 'Приемка и оплата',
subtitle: 'Подтвердите качество, чтобы закрыть этап и оплату.',
primaryLabel: 'Подтвердить качество',
onPrimary: function () {
state.accepted = true;
},
ready: function () {
return state.accepted;
},
content: function () {
var status = state.accepted ? 'Принято' : 'Ожидает';
var tagClass = state.accepted ? 'success' : 'info';
return (
'<div class="list">' +
'<div class="item"><strong>Инспекция</strong><span class="tag ' +
tagClass +
'">' +
status +
'</span></div>' +
'<div class="item"><strong>Эскроу</strong><span class="tag info">1 250 000 ₽</span></div>' +
'</div>'
);
},
helper: function () {
return state.accepted
? 'Качество подтверждено. Средства автоматически переводятся исполнителю.'
: 'Подтвердите качество, чтобы выпустить оплату и закрыть этап.';
}
}
];
var stepLabel = document.getElementById('stepLabel');
var stepTitle = document.getElementById('stepTitle');
var stepSubtitle = document.getElementById('stepSubtitle');
var stepContent = document.getElementById('stepContent');
var helperCard = document.getElementById('helperCard');
var progressBar = document.getElementById('progressBar');
var actions = document.querySelector('.actions');
function render() {
var step = steps[state.step];
stepLabel.textContent = step.label;
stepTitle.textContent = step.title;
stepSubtitle.textContent = step.subtitle;
stepContent.innerHTML = step.content();
helperCard.textContent = step.helper();
progressBar.style.width = ((state.step + 1) / steps.length) * 100 + '%';
var buttons = actions.querySelectorAll('button');
buttons.forEach(function (btn) {
var action = btn.getAttribute('data-action');
if (action === 'back') {
btn.disabled = state.step === 0;
}
if (action === 'primary') {
btn.textContent = step.primaryLabel || 'Действие';
btn.style.display = step.primaryLabel ? 'inline-flex' : 'none';
}
if (action === 'next') {
btn.textContent = state.step === steps.length - 1 ? 'Завершить' : 'Далее';
btn.disabled = !step.ready();
}
});
}
actions.addEventListener('click', function (event) {
var target = event.target;
if (!target || !target.getAttribute) return;
var action = target.getAttribute('data-action');
var step = steps[state.step];
if (action === 'back') {
state.step = Math.max(0, state.step - 1);
render();
return;
}
if (action === 'primary') {
if (step.onPrimary) {
step.onPrimary();
}
render();
return;
}
if (action === 'next') {
if (!step.ready()) return;
if (state.step >= steps.length - 1) {
resetState();
render();
return;
}
state.step += 1;
render();
}
});
render();
})();
<\\/script>
</body>
</html>`;
let granimInstance
onMounted(() => {
granimInstance = new Granim({
element: '#canvas-basic',
direction: 'radial',
isPausedWhenNotInView: true,
states: {
'default-state': {
gradients: [
['#ff9966', '#ff5e62'],
['#00F260', '#0575E6'],
['#e1eec3', '#f05053']
]
}
}
})
onUnmounted(() => {
if (granimInstance && granimInstance.pause) granimInstance.pause()
})
})
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@300;400;600;700&family=Space+Grotesk:wght@400;600;700&display=swap');
:root {
color-scheme: light;
}
body {
font-family: 'Manrope', system-ui, sans-serif;
}
html {
scroll-behavior: smooth;
}
.font-display {
font-family: 'Space Grotesk', system-ui, sans-serif;
}
.gradient-canvas {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
filter: saturate(1.1);
}
.nav-link {
transition: all 0.2s ease;
}
.nav-link:hover {
background: rgba(255, 255, 255, 0.5);
border-color: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(14px) saturate(1.1);
-webkit-backdrop-filter: blur(14px) saturate(1.1);
color: rgba(20, 20, 20, 0.85);
}
.slide {
border-radius: 2rem;
background: color-mix(in oklab, white 80%, transparent);
border: 1px solid rgba(20, 20, 20, 0.08);
padding: 2.5rem;
box-shadow: 0 18px 50px rgba(20, 20, 20, 0.08);
min-height: 70vh;
display: flex;
flex-direction: column;
}
.slide-top {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.2em;
color: rgba(20, 20, 20, 0.45);
margin-bottom: 1.25rem;
}
.slide-title {
font-family: 'Space Grotesk', system-ui, sans-serif;
font-size: clamp(2rem, 3vw, 2.75rem);
line-height: 1.1;
margin: 0;
}
.slide-preamble {
margin-top: 0.75rem;
color: rgba(20, 20, 20, 0.7);
font-size: 1rem;
max-width: 52rem;
}
.slide-head {
margin-bottom: 1.5rem;
}
.slide-main {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 1.5rem;
}
.glass-card {
background: rgba(255, 255, 255, 0.55);
border: 1px solid rgba(255, 255, 255, 0.45);
backdrop-filter: blur(18px) saturate(1.1);
-webkit-backdrop-filter: blur(18px) saturate(1.1);
}
.iphone-frame {
position: relative;
width: min(330px, 90vw);
aspect-ratio: 9 / 19.5;
padding: 14px;
border-radius: 44px;
background: linear-gradient(180deg, #141418, #0b0b0e);
box-shadow:
0 28px 60px rgba(9, 9, 12, 0.35),
inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.iphone-frame::before {
content: '';
position: absolute;
inset: 6px;
border-radius: 38px;
border: 1px solid rgba(255, 255, 255, 0.08);
pointer-events: none;
}
.iphone-screen {
position: relative;
height: 100%;
border-radius: 34px;
overflow: hidden;
background: #0b0b0e;
}
.iphone-iframe {
width: 100%;
height: 100%;
border: 0;
display: block;
position: relative;
z-index: 1;
pointer-events: auto;
}
.iphone-notch {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
width: 46%;
height: 24px;
border-radius: 0 0 18px 18px;
background: #0b0b0e;
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.35);
z-index: 2;
pointer-events: none;
}
.iphone-button {
position: absolute;
width: 3px;
border-radius: 999px;
background: #101014;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.iphone-button--mute {
height: 22px;
left: -4px;
top: 78px;
}
.iphone-button--vol-up {
height: 44px;
left: -4px;
top: 112px;
}
.iphone-button--vol-down {
height: 44px;
left: -4px;
top: 164px;
}
.iphone-button--power {
height: 60px;
right: -4px;
top: 140px;
}
</style>