diff --git a/site/app.py b/site/app.py
index d92fd99..1453307 100644
--- a/site/app.py
+++ b/site/app.py
@@ -19,7 +19,7 @@ from routes.api_scenario_run import bp_run as api_scenario_run_bp
from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp
# Версия — меняется при КАЖДОМ изменении кода. Показывается в топбаре UI.
-VERSION = "1.2.6"
+VERSION = "1.2.7"
app = Flask(__name__, template_folder="templates", static_folder="static")
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc")
diff --git a/site/static/js/icons.js b/site/static/js/icons.js
new file mode 100644
index 0000000..314accf
--- /dev/null
+++ b/site/static/js/icons.js
@@ -0,0 +1,16 @@
+// icons.js — 10 inline-SVG иконок (Lucide, MIT)
+// stroke="currentColor" — цвет берётся из CSS
+// Размер: 20x20, stroke-width: 2
+
+const ICONS = {
+ play: '',
+ edit: '',
+ trash: '',
+ check: '',
+ xIcon: '',
+ chevronDown: '',
+ search: '',
+ plus: '',
+ rotateCw: '',
+ cloud: '',
+};
diff --git a/site/static/js/snackbar.js b/site/static/js/snackbar.js
new file mode 100644
index 0000000..b6599d9
--- /dev/null
+++ b/site/static/js/snackbar.js
@@ -0,0 +1,30 @@
+// snackbar.js — лёгкие уведомления (без зависимостей)
+// showSnackbar(msg, type='info') — type: info, success, error
+
+function showSnackbar(msg, type) {
+ type = type || 'info';
+ let container = document.getElementById('snackbar-container');
+ if (!container) {
+ container = document.createElement('div');
+ container.id = 'snackbar-container';
+ container.className = 'snackbar-container';
+ container.setAttribute('role', 'status');
+ container.setAttribute('aria-live', 'polite');
+ document.body.appendChild(container);
+ }
+ const el = document.createElement('div');
+ el.className = 'snackbar ' + type;
+ el.textContent = msg;
+ el.onclick = function() { el.remove(); };
+ container.appendChild(el);
+
+ // Limit stacking
+ const all = container.querySelectorAll('.snackbar');
+ if (all.length > 4) all[0].remove();
+
+ // Auto-hide: info/success 4s, error 8s
+ const delay = (type === 'error') ? 8000 : 4000;
+ setTimeout(function() {
+ if (el.parentNode) el.remove();
+ }, delay);
+}
diff --git a/site/static/style.css b/site/static/style.css
index 56d2711..e4d2b20 100644
--- a/site/static/style.css
+++ b/site/static/style.css
@@ -150,3 +150,96 @@ tr:hover td {
color: #fff;
border-color: var(--destructive);
}
+
+/* === Phase 0: Redesign Foundation === */
+
+/* ── Sidebar ── */
+.sidebar {
+ position: fixed; top: 0; left: 0; bottom: 0;
+ width: 192px; background: var(--background);
+ border-right: 1px solid var(--brand-gray);
+ display: flex; flex-direction: column; z-index: 100;
+ transition: width .2s;
+}
+.sidebar.collapsed { width: 56px; }
+.sidebar-top {
+ height: 80px; border-top: 4px solid var(--brand-primary);
+ display: flex; align-items: center; padding: 0 16px;
+}
+.sidebar-nav { flex: 1; overflow-y: auto; padding: 8px 0; }
+.sidebar-item {
+ display: flex; align-items: center; gap: 12px;
+ padding: 10px 16px; font-size: 14px; color: var(--foreground);
+ cursor: pointer; transition: background .15s;
+ border: none; background: none; width: 100%; text-align: left;
+}
+.sidebar-item:hover { background: var(--brand-grey-light); }
+.sidebar-item.active { background: #dbeafe; font-weight: 600; }
+.sidebar-item svg { width: 20px; height: 20px; flex-shrink: 0; }
+.sidebar-bottom { padding: 8px 0; border-top: 1px solid var(--brand-gray); }
+
+/* ── Views ── */
+.view { margin-left: 192px; padding: 16px 20px; min-height: 100vh; }
+.view.hidden { display: none; }
+.view-header { font-size: 20px; font-weight: 700; margin-bottom: 16px; }
+
+/* ── Tabs ── */
+.tabs { display: flex; gap: 0; border-bottom: 2px solid var(--brand-gray); margin-bottom: 12px; }
+.tab {
+ padding: 8px 16px; font-size: 14px; color: var(--muted);
+ cursor: pointer; border: none; background: none;
+ border-bottom: 2px solid transparent; margin-bottom: -2px;
+ transition: color .15s, border-color .15s;
+}
+.tab:hover { color: var(--foreground); }
+.tab.active { color: var(--brand-primary); border-bottom-color: var(--brand-primary); font-weight: 600; }
+
+/* ── Toolbar ── */
+.toolbar { display: flex; gap: 6px; flex-wrap: wrap; padding: 8px 0; border-top: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray); margin: 8px 0; }
+
+/* ── Field (label on top) ── */
+.field { margin-bottom: 10px; }
+.field label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 4px; color: var(--foreground); }
+.field label.required::after { content: " *"; color: var(--destructive); }
+.field input, .field select, .field textarea {
+ width: 100%; padding: 8px 10px; font-size: 14px;
+ border: 1px solid var(--brand-gray); border-radius: var(--radius-sm);
+ background: var(--background);
+}
+.field input:focus, .field select:focus {
+ outline: none; border-color: var(--brand-primary);
+ box-shadow: 0 0 0 2px rgba(37,99,235,.15);
+}
+
+/* ── Skeleton ── */
+.skeleton {
+ background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
+ background-size: 200% 100%; animation: skeleton-shimmer 1.5s infinite;
+ border-radius: 4px;
+}
+@keyframes skeleton-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
+.skeleton-line { height: 14px; margin-bottom: 8px; }
+.skeleton-line:last-child { width: 60%; }
+
+/* ── Snackbar ── */
+.snackbar-container { position: fixed; bottom: 16px; right: 16px; z-index: 10001; display: flex; flex-direction: column; gap: 8px; }
+.snackbar {
+ padding: 10px 16px; border-radius: var(--radius-sm); font-size: 13px;
+ color: #fff; box-shadow: 0 2px 8px rgba(0,0,0,.2);
+ animation: snackbar-in .3s ease; max-width: 360px;
+}
+.snackbar.info { background: var(--brand-primary); }
+.snackbar.success { background: var(--green); }
+.snackbar.error { background: var(--destructive); }
+@keyframes snackbar-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
+
+/* ── Combobox ── */
+.combobox { position: relative; }
+.combobox-input { width: 100%; padding: 6px 10px; font-size: 13px; border: 1px solid var(--brand-gray); border-radius: var(--radius-sm); }
+.combobox-input:focus { outline: none; border-color: var(--brand-primary); }
+.combobox-list { position: absolute; top: 100%; left: 0; right: 0; max-height: 200px; overflow-y: auto; background: var(--background); border: 1px solid var(--brand-gray); border-radius: 0 0 var(--radius-sm) var(--radius-sm); z-index: 10; }
+.combobox-item { padding: 6px 10px; font-size: 13px; cursor: pointer; }
+.combobox-item:hover { background: var(--brand-grey-light); }
+
+/* ── Body for sidebar ── */
+body.has-sidebar { padding-left: 0; }
diff --git a/site/templates/index.html b/site/templates/index.html
index 32955ab..7d12457 100644
--- a/site/templates/index.html
+++ b/site/templates/index.html
@@ -151,6 +151,7 @@ window.APP = {
firstServiceId: {{ (config.service_ids[0] if config.service_ids else 1) | tojson }}
};
+