-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
173 lines (168 loc) · 30.4 KB
/
Copy pathindex.html
File metadata and controls
173 lines (168 loc) · 30.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#f3f0e9">
<title>UltraMenu — генератор лёгких цифровых меню</title>
<style>
:root{
--bg:#f1eee7;--surface:#fffdf9;--surface2:#f8f4ee;--surface3:#f4efe8;--text:#181714;--muted:#777169;
--line:#e2dbd1;--line2:#d4ccc1;--black:#1a1916;--danger:#a04c43;--radius:21px;
--shadow:0 18px 55px rgba(47,39,30,.075);--soft:0 6px 20px rgba(47,39,30,.045)
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;min-height:100vh;color:var(--text);font:14px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:
radial-gradient(circle at 8% -4%,rgba(255,255,255,.96),transparent 29%),
radial-gradient(circle at 96% 18%,rgba(231,224,213,.48),transparent 25%),
linear-gradient(180deg,#f5f2ec 0,var(--bg) 100%)}
button,input,select{font:inherit}button{cursor:pointer}::selection{background:#1b1a17;color:white}
button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid rgba(42,40,35,.12);outline-offset:1px}
.shell{min-height:100vh}
.topbar{height:70px;position:sticky;top:0;z-index:30;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:0 24px;border-bottom:1px solid rgba(215,208,197,.82);background:rgba(245,242,236,.83);backdrop-filter:blur(18px) saturate(1.2)}
.brand-wrap,.brand,.top-actions,.row,.device-switch,.mini-stats{display:flex;align-items:center}.brand-wrap{gap:12px}.brand{gap:10px;font-weight:830;letter-spacing:-.035em;font-size:18px}.brand-mark{width:33px;height:33px;display:grid;place-items:center;border-radius:11px;background:var(--black);color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,.08),0 6px 18px rgba(26,25,22,.15)}.brand-mark:before{content:"U";font-size:14px;font-weight:900}
.workspace-pill{font-size:10px;font-weight:720;letter-spacing:.06em;text-transform:uppercase;color:#8b857d;border-left:1px solid var(--line2);padding-left:12px}.top-actions{gap:8px}.btn{border:1px solid var(--line);background:rgba(255,253,249,.88);color:var(--text);min-height:38px;padding:8px 12px;border-radius:12px;box-shadow:0 1px 0 rgba(255,255,255,.75);transition:.16s ease}.btn:hover{transform:translateY(-1px);border-color:var(--line2);background:#fff;box-shadow:0 7px 18px rgba(42,35,28,.06)}.btn:active{transform:none;box-shadow:none}.btn.primary{background:var(--black);border-color:var(--black);color:#fff;box-shadow:0 8px 20px rgba(22,21,18,.15)}.btn.primary:hover{background:#292721;border-color:#292721}.btn.subtle{background:transparent}.btn.danger{color:var(--danger)}
.layout{display:grid;grid-template-columns:minmax(420px,548px) minmax(0,1fr);min-height:calc(100vh - 70px)}
.editor{border-right:1px solid rgba(216,209,199,.82);padding:24px;overflow:auto}.editor-head{padding:3px 2px 17px}.eyebrow{font-size:10px;font-weight:760;letter-spacing:.13em;text-transform:uppercase;color:#928b82;margin-bottom:5px}.editor-title{font-size:25px;line-height:1.12;letter-spacing:-.045em;margin:0 0 6px}.small{font-size:11.5px;color:var(--muted);line-height:1.48}.mini-stats{gap:7px;margin-top:12px;flex-wrap:wrap}.stat-pill,.badge{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);background:rgba(255,253,249,.7);color:#706a63;border-radius:999px}.stat-pill{padding:6px 9px;font-size:10.5px}.stat-pill strong{color:#27251f;font-size:11px}.badge{padding:5px 8px;font-size:10.5px}.badge:before{content:"";width:5px;height:5px;border-radius:50%;background:#7a9b78}.badge.busy:before{background:#b78b55}
.section{position:relative;margin-bottom:14px;padding:18px;border:1px solid rgba(221,214,204,.92);border-radius:var(--radius);background:rgba(255,253,249,.91);box-shadow:var(--soft);backdrop-filter:blur(8px)}.section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px}.section-title-wrap{display:flex;gap:10px;align-items:flex-start}.step{width:27px;height:27px;flex:0 0 27px;display:grid;place-items:center;border-radius:9px;background:#f0ebe3;border:1px solid #e1d9ce;color:#7d766e;font-size:10px;font-weight:800}.section h2{font-size:14px;line-height:1.25;letter-spacing:-.018em;margin:1px 0 3px;font-weight:790}.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}.field{display:flex;flex-direction:column;gap:6px;margin-bottom:11px}.field:last-child{margin-bottom:0}label{font-size:11px;color:var(--muted);font-weight:700;letter-spacing:.008em}input,select{width:100%;border:1px solid var(--line);background:rgba(255,255,255,.88);border-radius:12px;padding:10.5px 11.5px;color:var(--text);outline:none;transition:.15s ease}input:hover,select:hover{border-color:#d4ccc1;background:#fff}input:focus,select:focus{border-color:#aaa197;background:#fff;box-shadow:0 0 0 3px rgba(49,45,39,.05)}.row{gap:8px;flex-wrap:wrap}.between{justify-content:space-between}
.theme-picker{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}.theme-option{position:relative;border:1px solid var(--line);border-radius:12px;padding:8px;background:#fff;cursor:pointer;transition:.15s ease}.theme-option:hover{border-color:var(--line2);transform:translateY(-1px)}.theme-option.active{border-color:#878075;box-shadow:0 0 0 2px rgba(41,38,33,.06)}.theme-sample{height:32px;border-radius:8px;border:1px solid rgba(0,0,0,.05);margin-bottom:6px}.theme-option span{font-size:10.5px;color:#736d65}.theme-cream .theme-sample{background:#f7f3ec}.theme-light .theme-sample{background:#fff}.theme-dark .theme-sample{background:#161614}.hidden-select{position:absolute;opacity:0;pointer-events:none}
.category-list{display:grid;gap:10px}.cat{border:1px solid rgba(219,212,202,.96);border-radius:17px;background:linear-gradient(180deg,#faf7f2,#f6f2eb);box-shadow:inset 0 1px rgba(255,255,255,.72);overflow:hidden}.cat-head{display:grid;grid-template-columns:1fr auto;gap:9px;padding:11px}.cat-title-wrap{min-width:0}.cat-title-row{display:flex;align-items:center;gap:7px}.cat-index{font-size:9px;font-weight:850;color:#9a938a;letter-spacing:.07em}.cat-title-input{font-weight:790;letter-spacing:-.012em}.cat-meta{font-size:10px;color:#999188;margin:5px 2px 0}.cat-actions{display:flex;gap:6px;align-items:start}.iconbtn{width:38px;height:38px;flex:0 0 38px;padding:0;border-radius:11px;border:1px solid var(--line);background:#fff;color:#625d56;font-size:15px;line-height:1;transition:.15s ease}.iconbtn:hover{border-color:#c9c1b6;color:#181714;transform:translateY(-1px);box-shadow:0 5px 13px rgba(39,34,28,.06)}.iconbtn:disabled{opacity:.35;cursor:default;transform:none;box-shadow:none}.iconbtn.danger:hover{color:var(--danger);border-color:#dfc9c4}.dishes{padding:0 11px 11px}.dish{position:relative;display:grid;grid-template-columns:1fr 91px 38px;gap:7px;padding:10px;margin-top:7px;border:1px solid #e7e0d6;border-radius:14px;background:rgba(255,255,255,.72)}.dish:first-child{margin-top:0}.dish:focus-within{background:#fff;border-color:#d6cec3}.dish .desc{grid-column:1/3;color:#635e57}.dish-number{position:absolute;top:-6px;left:10px;background:#f7f3ed;color:#aaa198;padding:0 5px;font-size:8.5px;font-weight:800;letter-spacing:.06em}.add-dish-wide{width:100%;margin-top:8px;border:1px dashed #d9d1c6;background:rgba(255,255,255,.42);color:#797269;border-radius:12px;padding:9px;font-size:11px}.add-dish-wide:hover{background:#fff;border-style:solid}.empty{padding:24px 16px;text-align:center;border:1px dashed #d6cec3;border-radius:15px;color:var(--muted);background:rgba(255,255,255,.32)}.empty strong{display:block;color:#4d4842;font-size:12px;margin-bottom:3px}
.metric-grid{display:grid;grid-template-columns:1.15fr .85fr;gap:10px}.metric-card{border:1px solid var(--line);background:#faf7f2;border-radius:14px;padding:13px}.metric-label{font-size:10px;color:#8c857d;margin-bottom:5px}.metric{display:flex;align-items:flex-end;gap:6px}.metric strong{font-size:30px;line-height:.95;letter-spacing:-.065em}.metric span{font-size:11px;color:var(--muted)}.progress{height:5px;margin-top:10px;background:#e8e1d7;border-radius:99px;overflow:hidden}.progress i{display:block;width:25%;height:100%;background:#24221e;border-radius:inherit;transition:width .2s ease}.feature-list{display:grid;gap:6px}.feature{display:flex;align-items:center;gap:7px;font-size:10.5px;color:#777169}.feature:before{content:"✓";display:grid;place-items:center;width:16px;height:16px;border-radius:50%;background:#eeebe4;color:#514d47;font-size:9px;font-weight:900}.footer-note{padding:4px 3px 20px;color:#8b857d;font-size:10.5px}
.preview-wrap{height:calc(100vh - 70px);position:sticky;top:70px;padding:24px}.preview-stage{height:100%;display:flex;flex-direction:column}.preview-top{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 2px 11px}.preview-label strong{display:block;font-size:12px;letter-spacing:-.01em}.preview-label span{font-size:10.5px;color:var(--muted)}.device-switch{gap:4px;padding:4px;border:1px solid var(--line);background:rgba(255,253,249,.72);border-radius:11px}.device-btn{border:0;background:transparent;color:#817a72;font-size:10.5px;padding:6px 9px;border-radius:8px}.device-btn.active{background:#fff;color:#25231f;box-shadow:0 2px 8px rgba(42,35,28,.07)}.browser{min-height:0;flex:1;display:flex;flex-direction:column;background:linear-gradient(180deg,#1a1918,#0f0f0e);border:1px solid rgba(255,255,255,.08);border-radius:29px;padding:10px;box-shadow:0 30px 80px rgba(35,30,24,.17)}.browser-bar{height:43px;display:flex;align-items:center;gap:7px;padding:0 9px;color:#999}.dot{width:8px;height:8px;border-radius:50%;background:#494846}.preview-url{margin-left:8px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;color:#8c8b88}.browser-actions{margin-left:auto;display:flex;gap:5px}.browser-icon{width:25px;height:25px;display:grid;place-items:center;border-radius:8px;color:#777;font-size:12px}.iframe-holder{min-height:0;flex:1;display:flex;justify-content:center;align-items:stretch;border-radius:20px;overflow:hidden;background:#e9e9e9;transition:.22s ease}.iframe-holder.mobile{padding:18px;background:#222}.iframe-holder.mobile iframe{max-width:390px;border-radius:22px;box-shadow:0 10px 38px rgba(0,0,0,.25)}iframe{width:100%;height:100%;border:0;background:white;transition:max-width .22s ease}.preview-actions{display:flex;gap:7px;padding-top:10px}.preview-actions .btn{font-size:11px}.preview-actions .spacer{flex:1}
.toast{position:fixed;left:50%;bottom:24px;z-index:100;pointer-events:none;opacity:0;transform:translate(-50%,18px);background:#1b1a17;color:#fff;padding:10px 14px;border-radius:12px;box-shadow:0 12px 35px rgba(17,16,14,.2);transition:.2s}.toast.show{opacity:1;transform:translate(-50%,0)}
@media(max-width:1000px){.layout{grid-template-columns:minmax(390px,500px) minmax(0,1fr)}.workspace-pill{display:none}.preview-wrap{padding:18px}}
@media(max-width:860px){.layout{grid-template-columns:1fr}.editor{border-right:0}.preview-wrap{position:relative;top:auto;height:720px;padding-top:0}.preview-top{padding-top:4px}}
@media(max-width:560px){.topbar{height:62px;padding:0 12px}.brand{font-size:17px}.brand-mark{width:30px;height:30px}.top-actions .optional{display:none}.btn{min-height:36px;padding:8px 10px}.editor{padding:12px}.editor-title{font-size:23px}.section{padding:15px;border-radius:18px}.grid2,.metric-grid{grid-template-columns:1fr}.theme-picker{grid-template-columns:repeat(3,1fr)}.cat-head{grid-template-columns:1fr}.cat-actions{justify-content:flex-end}.dish{grid-template-columns:1fr 80px 38px}.preview-wrap{height:690px;padding:0 12px 12px}.preview-top{padding-top:8px}.browser{border-radius:23px}.iframe-holder.mobile{padding:10px}.preview-actions .optional-mobile{display:none}}
</style>
</head>
<body>
<div class="shell">
<header class="topbar">
<div class="brand-wrap">
<div class="brand"><span class="brand-mark"></span> UltraMenu</div>
<div class="workspace-pill">Lightweight menu studio</div>
</div>
<div class="top-actions">
<button class="btn optional" id="exportJson">Экспорт</button>
<button class="btn optional" id="importJson">Импорт</button>
<button class="btn primary" id="downloadMenu">Скачать меню</button>
<input type="file" id="fileInput" accept=".json,application/json" hidden>
</div>
</header>
<main class="layout">
<section class="editor">
<div class="editor-head">
<div class="eyebrow">Редактор меню</div>
<h1 class="editor-title">Соберите меню за пару минут.</h1>
<div class="small">Без тяжёлых библиотек. Всё, что видит гость, превращается в один быстрый HTML-файл.</div>
<div class="mini-stats">
<span class="stat-pill"><strong id="statCats">0</strong> категорий</span>
<span class="stat-pill"><strong id="statDishes">0</strong> блюд</span>
<span class="stat-pill"><strong id="statWeight">0 KB</strong> итоговый вес</span>
</div>
</div>
<div class="section">
<div class="section-head">
<div class="section-title-wrap"><span class="step">01</span><div><h2>О заведении</h2><div class="small">Название и короткая информация для гостей.</div></div></div>
<span class="badge" id="saveState">сохранено</span>
</div>
<div class="field"><label>Название</label><input id="venueName" value="BISTRO 21" maxlength="60" placeholder="Название заведения"></div>
<div class="grid2">
<div class="field"><label>Подзаголовок</label><input id="venueSubtitle" value="Европейская кухня" placeholder="Например, Европейская кухня"></div>
<div class="field"><label>Режим работы</label><input id="venueHours" value="Ежедневно · 10:00–23:00" placeholder="Ежедневно · 10:00–23:00"></div>
</div>
<div class="grid2">
<div class="field"><label>Валюта</label><select id="currency"><option value="₽">₽ RUB</option><option value="€">€ EUR</option><option value="$">$ USD</option><option value="₸">₸ KZT</option></select></div>
<div class="field"><label>Оформление</label><div class="theme-picker" id="themePicker"><button type="button" class="theme-option theme-cream" data-theme="cream"><div class="theme-sample"></div><span>Тёплая</span></button><button type="button" class="theme-option theme-light" data-theme="light"><div class="theme-sample"></div><span>Светлая</span></button><button type="button" class="theme-option theme-dark" data-theme="dark"><div class="theme-sample"></div><span>Тёмная</span></button></div><select id="theme" class="hidden-select" tabindex="-1"><option value="cream">cream</option><option value="light">light</option><option value="dark">dark</option></select></div>
</div>
</div>
<div class="section">
<div class="section-head">
<div class="section-title-wrap"><span class="step">02</span><div><h2>Категории и блюда</h2><div class="small">Расположите категории в нужном порядке и заполните позиции.</div></div></div>
<button class="btn" id="addCategory">+ Категория</button>
</div>
<div id="categories" class="category-list"></div>
</div>
<div class="section">
<div class="section-head">
<div class="section-title-wrap"><span class="step">03</span><div><h2>Проверка скорости</h2><div class="small">Вес считается по реальному HTML, который скачает заведение.</div></div></div>
<span class="badge" id="speedBadge">молниеносно</span>
</div>
<div class="metric-grid">
<div class="metric-card"><div class="metric-label">Вес страницы</div><div class="metric"><strong id="weight">0</strong><span>KB</span></div><div class="progress"><i id="weightProgress"></i></div><div class="small" style="margin-top:8px" id="speedText"></div></div>
<div class="metric-card"><div class="metric-label">Внутри готового файла</div><div class="feature-list"><div class="feature">HTML + CSS</div><div class="feature">Без библиотек</div><div class="feature">Работает офлайн</div><div class="feature">Адаптивный дизайн</div></div></div>
</div>
</div>
<div class="footer-note">UltraMenu сохраняет проект только в браузере. Готовая страница остаётся полностью автономной.</div>
</section>
<section class="preview-wrap">
<div class="preview-stage">
<div class="preview-top">
<div class="preview-label"><strong>Живой предпросмотр</strong><span>Обновляется сразу после изменения</span></div>
<div class="device-switch"><button class="device-btn active" data-device="desktop">Широко</button><button class="device-btn" data-device="mobile">Телефон</button></div>
</div>
<div class="browser">
<div class="browser-bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="preview-url" id="previewUrl">menu.local/bistro-21</span><div class="browser-actions"><span class="browser-icon">↗</span></div></div>
<div class="iframe-holder" id="iframeHolder"><iframe id="preview" title="Предпросмотр меню"></iframe></div>
</div>
<div class="preview-actions"><button class="btn" id="copyHtml">Копировать HTML</button><button class="btn optional-mobile" id="resetDemo">Вернуть пример</button><span class="spacer"></span><button class="btn primary" id="downloadMenuBottom">Скачать .html</button></div>
</div>
</section>
</main>
</div>
<div class="toast" id="toast"></div>
<script>
const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];
const demoState={venueName:"BISTRO 21",venueSubtitle:"Европейская кухня",venueHours:"Ежедневно · 10:00–23:00",currency:"₽",theme:"cream",categories:[{name:"Завтраки",dishes:[{name:"Сырники",desc:"Сметана, ягодный соус",price:"390"},{name:"Омлет",desc:"Три яйца, зелень, томаты",price:"350"}]},{name:"Горячее",dishes:[{name:"Паста Карбонара",desc:"Спагетти, бекон, пармезан, сливочный соус",price:"590"},{name:"Курица гриль",desc:"Картофель, сезонные овощи",price:"620"}]},{name:"Напитки",dishes:[{name:"Капучино",desc:"250 мл",price:"240"},{name:"Лимонад",desc:"Цитрус, мята · 400 мл",price:"290"}]}]};
const state=JSON.parse(JSON.stringify(demoState));
function esc(v=""){return String(v).replace(/[&<>"']/g,m=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[m]))}
function slugify(v){return String(v).toLowerCase().trim().replace(/[^a-zа-яё0-9]+/gi,"-").replace(/^-|-$/g,"")||"menu"}
function notify(text){const t=$("#toast");t.textContent=text;t.classList.add("show");clearTimeout(notify.t);notify.t=setTimeout(()=>t.classList.remove("show"),1800)}
function save(){localStorage.setItem("ultramenu_state",JSON.stringify(state));const s=$("#saveState");s.textContent="сохранено";s.classList.remove("busy")}
function markDirty(){const s=$("#saveState");s.textContent="сохраняю…";s.classList.add("busy");clearTimeout(markDirty.t);markDirty.t=setTimeout(save,250)}
function dishCount(){return state.categories.reduce((n,c)=>n+c.dishes.length,0)}
function syncTheme(){ $$(".theme-option").forEach(b=>b.classList.toggle("active",b.dataset.theme===state.theme));$("#theme").value=state.theme }
function renderEditor(){
$("#venueName").value=state.venueName;$("#venueSubtitle").value=state.venueSubtitle;$("#venueHours").value=state.venueHours;$("#currency").value=state.currency;syncTheme();
const box=$("#categories");box.innerHTML="";
if(!state.categories.length)box.innerHTML='<div class="empty"><strong>Категорий пока нет</strong>Добавьте первую категорию и начните заполнять меню.</div>';
state.categories.forEach((cat,ci)=>{
const el=document.createElement("div");el.className="cat";
el.innerHTML=`<div class="cat-head"><div class="cat-title-wrap"><div class="cat-title-row"><span class="cat-index">${String(ci+1).padStart(2,"0")}</span><input class="cat-title-input" data-action="cat-name" data-ci="${ci}" value="${esc(cat.name)}" aria-label="Название категории"></div><div class="cat-meta">${cat.dishes.length} ${plural(cat.dishes.length,"позиция","позиции","позиций")}</div></div><div class="cat-actions"><button class="iconbtn" data-action="up-cat" data-ci="${ci}" title="Выше" ${ci===0?"disabled":""}>↑</button><button class="iconbtn" data-action="down-cat" data-ci="${ci}" title="Ниже" ${ci===state.categories.length-1?"disabled":""}>↓</button><button class="iconbtn danger" data-action="del-cat" data-ci="${ci}" title="Удалить категорию">×</button></div></div><div class="dishes">${cat.dishes.map((d,di)=>`<div class="dish"><span class="dish-number">${String(di+1).padStart(2,"0")}</span><input data-action="dish-name" data-ci="${ci}" data-di="${di}" value="${esc(d.name)}" placeholder="Название блюда"><input data-action="dish-price" data-ci="${ci}" data-di="${di}" value="${esc(d.price)}" inputmode="decimal" placeholder="Цена"><button class="iconbtn danger" data-action="del-dish" data-ci="${ci}" data-di="${di}" title="Удалить">×</button><input class="desc" data-action="dish-desc" data-ci="${ci}" data-di="${di}" value="${esc(d.desc)}" placeholder="Короткое описание"></div>`).join("")}<button class="add-dish-wide" data-action="add-dish" data-ci="${ci}">+ Добавить блюдо</button></div>`;
box.appendChild(el)
});
$("#statCats").textContent=state.categories.length;$("#statDishes").textContent=dishCount()
}
function plural(n,a,b,c){n=Math.abs(n)%100;const n1=n%10;if(n>10&&n<20)return c;if(n1>1&&n1<5)return b;if(n1===1)return a;return c}
function themeVars(theme){if(theme==="dark")return{bg:"#10100f",text:"#f4f1eb",muted:"#aaa69f",line:"#2b2a27",chip:"#20201d",soft:"#171715"};if(theme==="light")return{bg:"#ffffff",text:"#151515",muted:"#707070",line:"#e9e9e9",chip:"#f6f6f6",soft:"#fafafa"};return{bg:"#f7f3ec",text:"#191814",muted:"#777169",line:"#e6ded2",chip:"#efe8dd",soft:"#fffdf8"}}
function generateMenuHTML(){
const t=themeVars(state.theme),total=dishCount();
const nav=state.categories.map((c,i)=>`<a href="#c${i}">${esc(c.name)}</a>`).join("");
const cats=state.categories.map((cat,ci)=>`<section id="c${ci}" class="cat"><div class="cathead"><div><span>${String(ci+1).padStart(2,"0")}</span><h2>${esc(cat.name)}</h2></div><small>${cat.dishes.length} ${plural(cat.dishes.length,"позиция","позиции","позиций")}</small></div>${cat.dishes.map(d=>`<article class="dish"><div><h3>${esc(d.name)}</h3>${d.desc?`<p>${esc(d.desc)}</p>`:""}</div><strong>${esc(d.price)} ${esc(state.currency)}</strong></article>`).join("")}</section>`).join("");
return `<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="${t.bg}"><title>${esc(state.venueName)} — меню</title><style>*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:${t.bg};color:${t.text};font:15px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}main{max-width:760px;margin:auto;padding:38px 18px 64px}.hero{padding:10px 0 27px}.eyebrow{font-size:9px;font-weight:800;letter-spacing:.18em;text-transform:uppercase;color:${t.muted};margin-bottom:10px}h1{font-size:38px;line-height:1;letter-spacing:-.055em;margin:0 0 12px}.meta{display:flex;gap:7px;flex-wrap:wrap;color:${t.muted};font-size:12px}.meta span{padding:7px 9px;border:1px solid ${t.line};border-radius:999px;background:${t.soft}}nav{position:sticky;top:0;z-index:5;display:flex;gap:7px;overflow:auto;padding:11px 0;background:${t.bg};border-bottom:1px solid ${t.line};scrollbar-width:none}nav::-webkit-scrollbar{display:none}nav a{white-space:nowrap;text-decoration:none;color:${t.text};background:${t.chip};padding:8px 11px;border-radius:999px;font-size:12px}.cat{scroll-margin-top:59px;padding-top:31px}.cathead{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;padding-bottom:7px}.cathead>div{display:flex;align-items:baseline;gap:9px}.cathead span{font-size:9px;font-weight:850;color:${t.muted};letter-spacing:.08em}.cathead h2{font-size:19px;letter-spacing:-.035em;margin:0}.cathead small{font-size:10px;color:${t.muted}}.dish{display:flex;justify-content:space-between;gap:18px;padding:17px 0;border-bottom:1px solid ${t.line}}.dish h3{font-size:15.5px;letter-spacing:-.018em;margin:0 0 4px}.dish p{font-size:12.5px;color:${t.muted};margin:0;max-width:520px}.dish strong{white-space:nowrap;font-size:14.5px}footer{display:flex;justify-content:space-between;gap:10px;padding-top:36px;color:${t.muted};font-size:10px}@media(max-width:520px){main{padding:27px 16px 50px}h1{font-size:32px}.hero{padding-bottom:20px}.dish{gap:12px}.cathead h2{font-size:18px}}</style></head><body><main><header class="hero"><div class="eyebrow">Digital menu · ${total} ${plural(total,"позиция","позиции","позиций")}</div><h1>${esc(state.venueName)}</h1><div class="meta">${state.venueSubtitle?`<span>${esc(state.venueSubtitle)}</span>`:""}${state.venueHours?`<span>${esc(state.venueHours)}</span>`:""}</div></header>${nav?`<nav>${nav}</nav>`:""}${cats}<footer><span>UltraMenu</span><span>Lightweight digital menu</span></footer></main></body></html>`
}
function updatePreview(){
const html=generateMenuHTML();$("#preview").srcdoc=html;const kb=new Blob([html]).size/1024;$("#weight").textContent=kb.toFixed(1);$("#statWeight").textContent=kb.toFixed(1)+" KB";$("#previewUrl").textContent="menu.local/"+slugify(state.venueName);const pct=Math.min(100,Math.max(6,kb/50*100));$("#weightProgress").style.width=pct+"%";
if(kb<15){$("#speedBadge").textContent="молниеносно";$("#speedText").textContent="Очень маленькая страница — отлично для слабого мобильного интернета."}else if(kb<30){$("#speedBadge").textContent="очень быстро";$("#speedText").textContent="Страница остаётся значительно легче обычного PDF-меню."}else{$("#speedBadge").textContent="быстро";$("#speedText").textContent="Меню всё ещё лёгкое. Размер растёт в основном из-за текста."}
}
function refresh(){renderEditor();updatePreview();markDirty()}
["venueName","venueSubtitle","venueHours"].forEach(id=>$("#"+id).addEventListener("input",e=>{state[id]=e.target.value;updatePreview();markDirty()}));
$("#currency").addEventListener("change",e=>{state.currency=e.target.value;updatePreview();markDirty()});
$("#themePicker").addEventListener("click",e=>{const b=e.target.closest(".theme-option");if(!b)return;state.theme=b.dataset.theme;syncTheme();updatePreview();markDirty()});
$("#addCategory").onclick=()=>{state.categories.push({name:"Новая категория",dishes:[]});refresh()};
$("#categories").addEventListener("input",e=>{const a=e.target.dataset.action,ci=+e.target.dataset.ci,di=+e.target.dataset.di;if(a==="cat-name")state.categories[ci].name=e.target.value;if(a==="dish-name")state.categories[ci].dishes[di].name=e.target.value;if(a==="dish-price")state.categories[ci].dishes[di].price=e.target.value;if(a==="dish-desc")state.categories[ci].dishes[di].desc=e.target.value;updatePreview();markDirty()});
$("#categories").addEventListener("click",e=>{const b=e.target.closest("button");if(!b)return;const a=b.dataset.action,ci=+b.dataset.ci,di=+b.dataset.di;if(a==="add-dish")state.categories[ci].dishes.push({name:"Новое блюдо",desc:"",price:"0"});if(a==="del-dish")state.categories[ci].dishes.splice(di,1);if(a==="del-cat")state.categories.splice(ci,1);if(a==="up-cat"&&ci>0)[state.categories[ci-1],state.categories[ci]]=[state.categories[ci],state.categories[ci-1]];if(a==="down-cat"&&ci<state.categories.length-1)[state.categories[ci+1],state.categories[ci]]=[state.categories[ci],state.categories[ci+1]];refresh()});
function download(name,text,type){const blob=new Blob([text],{type}),a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
function downloadMenu(){download(slugify(state.venueName)+".html",generateMenuHTML(),"text/html;charset=utf-8");notify("Готовое меню скачано")}
$("#downloadMenu").onclick=downloadMenu;$("#downloadMenuBottom").onclick=downloadMenu;
$("#exportJson").onclick=()=>download(slugify(state.venueName)+".json",JSON.stringify(state,null,2),"application/json");$("#importJson").onclick=()=>$("#fileInput").click();
$("#fileInput").onchange=e=>{const f=e.target.files[0];if(!f)return;const r=new FileReader();r.onload=()=>{try{const d=JSON.parse(r.result);Object.keys(state).forEach(k=>delete state[k]);Object.assign(state,d);refresh();notify("Проект импортирован")}catch{notify("Не удалось прочитать JSON")}};r.readAsText(f)};
$("#copyHtml").onclick=async()=>{const html=generateMenuHTML();try{if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(html)}else{const t=document.createElement("textarea");t.value=html;t.style.position="fixed";t.style.opacity="0";document.body.appendChild(t);t.select();document.execCommand("copy");t.remove()}notify("HTML скопирован")}catch{notify("Не удалось скопировать HTML")}};
$("#resetDemo").onclick=()=>{if(!confirm("Вернуть демонстрационный пример? Текущие изменения будут заменены."))return;Object.keys(state).forEach(k=>delete state[k]);Object.assign(state,JSON.parse(JSON.stringify(demoState)));refresh();notify("Пример восстановлен")};
$$('.device-btn').forEach(b=>b.onclick=()=>{$$('.device-btn').forEach(x=>x.classList.remove('active'));b.classList.add('active');$('#iframeHolder').classList.toggle('mobile',b.dataset.device==='mobile')});
try{const saved=JSON.parse(localStorage.getItem("ultramenu_state")||"null");if(saved&&saved.categories){Object.keys(state).forEach(k=>delete state[k]);Object.assign(state,saved)}}catch{}
renderEditor();updatePreview();
</script>
</body>
</html>