я косяк, поправил веб. Капитан, работайте!
Co-authored-by: Cursor <cursoragent@cursor.com>
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,35 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/static/fonts/inter/inter-300.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/static/fonts/inter/inter-400.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/static/fonts/inter/inter-500.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('/static/fonts/inter/inter-600.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/static/fonts/inter/inter-700.ttf') format('truetype');
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
.kiosk-calibration-backdrop {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(26, 30, 28, 0.45);
|
||||
z-index: 5000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.kiosk-calibration-backdrop.is-open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.kiosk-calibration-panel {
|
||||
width: min(920px, 100%);
|
||||
max-height: min(92vh, 900px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 16px 48px rgba(26, 30, 28, 0.18);
|
||||
}
|
||||
|
||||
.kiosk-calibration-panel--dark {
|
||||
background: #1f1f1f;
|
||||
color: #fff;
|
||||
border: 1px solid #2f2f2f;
|
||||
}
|
||||
|
||||
.kiosk-calibration-panel--light {
|
||||
background: #fff;
|
||||
color: #1a1e1c;
|
||||
border: 1px solid rgba(26, 30, 28, 0.08);
|
||||
}
|
||||
|
||||
.kiosk-calibration-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kiosk-calibration-panel--dark .kiosk-calibration-head {
|
||||
border-bottom: 1px solid #2f2f2f;
|
||||
}
|
||||
|
||||
.kiosk-calibration-panel--light .kiosk-calibration-head {
|
||||
border-bottom: 1px solid rgba(26, 30, 28, 0.08);
|
||||
}
|
||||
|
||||
.kiosk-calibration-title {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.kiosk-calibration-close {
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kiosk-calibration-panel--dark .kiosk-calibration-close {
|
||||
border: 1px solid #7a7a7a;
|
||||
background: #3a3a3a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.kiosk-calibration-panel--light .kiosk-calibration-close {
|
||||
border: 1px solid rgba(26, 30, 28, 0.14);
|
||||
background: #fff;
|
||||
color: #1a1e1c;
|
||||
}
|
||||
|
||||
.kiosk-calibration-frame {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
min-height: 320px;
|
||||
max-height: calc(92vh - 64px);
|
||||
background: #fff;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/* Киоск: скрыть указатель мыши (Chromium + labwc). */
|
||||
html.wesp-kiosk-hide-cursor,
|
||||
html.wesp-kiosk-hide-cursor body,
|
||||
html.wesp-kiosk-hide-cursor * {
|
||||
cursor: none !important;
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
/**
|
||||
* Тема киоск-модалок: в светлой теме (html[data-theme="light"]) шапки и панели
|
||||
* совпадают со светлым интерфейсом, без «тёмного» вида заголовков.
|
||||
*/
|
||||
|
||||
/* --- Меню ☰ --- */
|
||||
html[data-theme="light"] #wespKioskMenuOverlay .wesp-kiosk-menu-panel {
|
||||
background: #ffffff !important;
|
||||
border-color: #e5e7eb !important;
|
||||
color: #111827 !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12) !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #wespKioskMenuOverlay .wesp-kiosk-menu-link {
|
||||
background: #f3f4f6 !important;
|
||||
border-color: #e5e7eb !important;
|
||||
color: #111827 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #wespKioskMenuOverlay .wesp-kiosk-menu-link:hover {
|
||||
background: #1b66a8 !important;
|
||||
border-color: #4ea8ff !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #wespKioskMenuOverlay .wesp-kiosk-menu-close {
|
||||
border-color: #d1d5db !important;
|
||||
background: #f3f4f6 !important;
|
||||
color: #111827 !important;
|
||||
}
|
||||
|
||||
/* --- Проверка БД / калибровка (iframe) --- */
|
||||
html[data-theme="light"] #kioskDbCheckModal .kiosk-db-check-panel,
|
||||
html[data-theme="light"] #kioskCalibrationModal .kiosk-calibration-panel {
|
||||
background: #ffffff !important;
|
||||
color: #111827 !important;
|
||||
border-color: #e5e7eb !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12) !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskDbCheckModal .kiosk-db-check-panel > div:first-child,
|
||||
html[data-theme="light"] #kioskCalibrationModal .kiosk-calibration-panel > div:first-child {
|
||||
border-bottom-color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskDbCheckModal .kiosk-db-check-panel h2,
|
||||
html[data-theme="light"] #kioskCalibrationModal .kiosk-calibration-panel h2 {
|
||||
color: #111827 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskDbCheckClose,
|
||||
html[data-theme="light"] #kioskCalibrationClose {
|
||||
border-color: #d1d5db !important;
|
||||
background: #f3f4f6 !important;
|
||||
color: #111827 !important;
|
||||
}
|
||||
|
||||
/* --- Привязка терминала (QR) --- */
|
||||
@keyframes kioskPairSpin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
#kioskPairModal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 5000;
|
||||
box-sizing: border-box;
|
||||
padding: 8px;
|
||||
padding:
|
||||
max(8px, env(safe-area-inset-top, 0px))
|
||||
max(8px, env(safe-area-inset-right, 0px))
|
||||
max(8px, env(safe-area-inset-bottom, 0px))
|
||||
max(8px, env(safe-area-inset-left, 0px));
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#kioskPairModal.kiosk-pair-open {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
body.kiosk-pair-modal-open {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-panel {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(480px, 100%);
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
min-height: 0;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
background: #1f1f1f;
|
||||
color: #fff;
|
||||
border-radius: 14px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
border: 1px solid #2f2f2f;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-header {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
#kioskPairModal h3 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: clamp(0.95rem, 3.6vw, 1.1rem);
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-subtitle {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: clamp(0.8rem, 3vw, 0.9rem);
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-steps {
|
||||
flex: 0 0 auto;
|
||||
margin: 0 auto 8px auto;
|
||||
text-align: left;
|
||||
max-width: 400px;
|
||||
font-size: clamp(0.72rem, 2.6vw, 0.8rem);
|
||||
line-height: 1.35;
|
||||
color: #d7d7d7;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-loading {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 8px auto;
|
||||
color: #d7d7d7;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-spinner {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #5a5a5a;
|
||||
border-top-color: #4ea8ff;
|
||||
border-radius: 50%;
|
||||
animation: kioskPairSpin 0.8s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-qr-wrap {
|
||||
display: none;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
margin: 4px auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-qr-wrap.kiosk-pair-qr-visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-qr {
|
||||
display: block;
|
||||
width: min(92vw, 52vh, 360px);
|
||||
min-width: min(160px, 78vw);
|
||||
height: auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
border: 1px solid #3b3b3b;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-url {
|
||||
flex: 0 0 auto;
|
||||
font-size: clamp(0.62rem, 2.2vw, 0.72rem);
|
||||
word-break: break-all;
|
||||
margin: 6px 0;
|
||||
color: #b8d8ff;
|
||||
text-align: left;
|
||||
max-height: 3.6em;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-status {
|
||||
flex: 0 0 auto;
|
||||
font-size: clamp(0.72rem, 2.6vw, 0.8rem);
|
||||
color: #d7d7d7;
|
||||
margin: 4px 0 6px;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-actions {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: stretch;
|
||||
align-items: stretch;
|
||||
flex-wrap: nowrap;
|
||||
margin: 8px -12px -12px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid #333;
|
||||
border-radius: 0 0 14px 14px;
|
||||
background: #1f1f1f;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-btn {
|
||||
box-sizing: border-box;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #7a7a7a;
|
||||
background: #3a3a3a;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: clamp(0.75rem, 2.8vw, 0.875rem);
|
||||
line-height: 1.25;
|
||||
min-height: 42px;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-btn--primary {
|
||||
border-color: #4ea8ff;
|
||||
background: #1b66a8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-btn--secondary {
|
||||
border-color: #5a5a5a;
|
||||
background: #2a2a2a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-btn--ghost {
|
||||
border-color: #6a6a6a;
|
||||
background: #333;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-btn:focus-visible {
|
||||
outline: 2px solid #4ea8ff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (max-height: 520px) {
|
||||
#kioskPairModal .kiosk-pair-panel {
|
||||
padding: 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-qr {
|
||||
width: min(88vw, 38vh, 280px);
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-steps {
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-btn {
|
||||
min-height: 38px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-btn {
|
||||
flex: 1 1 calc(50% - 4px);
|
||||
}
|
||||
|
||||
#kioskPairModal .kiosk-pair-btn--ghost {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#kioskBlockedOverlay {
|
||||
box-sizing: border-box;
|
||||
padding: max(16px, env(safe-area-inset-top, 0px)) 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#kioskBlockedOverlay > div {
|
||||
max-width: min(520px, 100%);
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskPairModal .kiosk-pair-panel {
|
||||
background: #ffffff !important;
|
||||
color: #111827 !important;
|
||||
border-color: #e5e7eb !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12) !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskPairModal h3 {
|
||||
color: #111827 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskPairModal .kiosk-pair-subtitle {
|
||||
color: #374151 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskPairModal .kiosk-pair-steps {
|
||||
color: #4b5563 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskPairLoading,
|
||||
html[data-theme="light"] #kioskPairModal .kiosk-pair-loading {
|
||||
color: #4b5563 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskPairLoading span:first-child,
|
||||
html[data-theme="light"] #kioskPairModal .kiosk-pair-spinner {
|
||||
border-color: #d1d5db !important;
|
||||
border-top-color: #2563eb !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskPairUrl {
|
||||
color: #2563eb !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskPairStatus {
|
||||
color: #6b7280 !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #kioskPairStatus {
|
||||
color: #d7d7d7 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskPairModal .kiosk-pair-actions {
|
||||
background: #ffffff !important;
|
||||
border-top-color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskPairModal .kiosk-pair-btn--primary,
|
||||
html[data-theme="light"] #kioskPairCopy {
|
||||
border-color: #2563eb !important;
|
||||
background: #2563eb !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskPairModal .kiosk-pair-btn--secondary,
|
||||
html[data-theme="light"] #kioskPairRefresh {
|
||||
border-color: #d1d5db !important;
|
||||
background: #ffffff !important;
|
||||
color: #111827 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskPairModal .kiosk-pair-btn--ghost,
|
||||
html[data-theme="light"] #kioskPairClose {
|
||||
border-color: #d1d5db !important;
|
||||
background: #f3f4f6 !important;
|
||||
color: #111827 !important;
|
||||
}
|
||||
|
||||
/* --- Блокировка до привязки --- */
|
||||
html[data-theme="light"] #kioskBlockedOverlay {
|
||||
background: rgba(15, 23, 42, 0.45) !important;
|
||||
color: #111827 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskBlockedOverlay h2 {
|
||||
color: #111827 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskBlockedOverlay p {
|
||||
color: #4b5563 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #kioskBlockedPairBtn {
|
||||
background: #2563eb !important;
|
||||
border: 1px solid #2563eb !important;
|
||||
color: #ffffff !important;
|
||||
padding: 10px 18px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url('/static/fonts/roboto/roboto-400.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url('/static/fonts/roboto/roboto-500.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url('/static/fonts/roboto/roboto-700.ttf') format('truetype');
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/* Карточка в главных настройках + модалка контроля отклонений */
|
||||
|
||||
/* Поверх #settingsModal (z-index: 3000) */
|
||||
#feedQualitySettingsModal.modal.wesp-shell-modal {
|
||||
z-index: 3100;
|
||||
}
|
||||
|
||||
.modal.wesp-shell-modal .modal-dialog.modal-dialog--shell-fq {
|
||||
max-width: min(760px, calc(100vw - 1rem));
|
||||
}
|
||||
|
||||
#feedQualitySettingsModal .modal-content.fq-settings-modal__content {
|
||||
max-height: min(94vh, 900px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#feedQualitySettingsModal .wesp-shell-bsmodal-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
max-height: min(calc(94vh - 8rem), 760px);
|
||||
}
|
||||
|
||||
#feedQualitySettingsModal .wesp-shell-bsmodal-actions {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border-light, #e2e8f0);
|
||||
}
|
||||
|
||||
.settings-feed-quality-card {
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.settings-feed-quality-card--clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.settings-feed-quality-card--clickable:hover {
|
||||
background: rgba(var(--primary-rgb), 0.05);
|
||||
border-color: var(--primary-color, #2196f3);
|
||||
}
|
||||
|
||||
.settings-feed-quality-card .settings-card-title {
|
||||
margin: 0;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.settings-feed-quality-card .settings-card-title > i:first-child {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.settings-feed-quality-card .settings-card-title .settings-chevron {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.fq-settings-modal .fq-settings-section {
|
||||
border: 1px solid var(--border-light, #e2e8f0);
|
||||
border-radius: 10px;
|
||||
padding: 0.85rem 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: var(--bg-light, #f8fafc);
|
||||
}
|
||||
|
||||
.fq-settings-modal .fq-settings-section__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.65rem;
|
||||
}
|
||||
|
||||
.fq-settings-modal .fq-settings-section__title {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1e293b);
|
||||
}
|
||||
|
||||
.fq-settings-modal .fq-settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.65rem 1rem;
|
||||
}
|
||||
|
||||
.fq-settings-modal .fq-settings-field label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted, #64748b);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.fq-settings-modal .fq-settings-checks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem 1.25rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.fq-settings-modal .fq-settings-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-dark, #334155);
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .settings-feed-quality-card {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: var(--dm-border, rgba(255, 255, 255, 0.12));
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .settings-feed-quality-card--clickable:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .fq-settings-modal .fq-settings-section {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .fq-settings-modal .fq-settings-section__title,
|
||||
html[data-theme="dark"] .fq-settings-modal .fq-settings-check {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Kiosk/operator skeleton layouts — builds on wesp-recipes-skeleton.css primitives.
|
||||
*/
|
||||
|
||||
/* --- Grid cards (direct children of .dispenser-grid) --- */
|
||||
|
||||
.dispenser-card.kiosk-skeleton-card {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dispenser-card.kiosk-skeleton-card:nth-child(1) { --skel-i: 0; }
|
||||
.dispenser-card.kiosk-skeleton-card:nth-child(2) { --skel-i: 1; }
|
||||
.dispenser-card.kiosk-skeleton-card:nth-child(3) { --skel-i: 2; }
|
||||
.dispenser-card.kiosk-skeleton-card:nth-child(4) { --skel-i: 3; }
|
||||
|
||||
.dispenser-grid.kiosk-skeleton-host--exit {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.dashboard-skeleton-bar--kiosk-title {
|
||||
width: 72%;
|
||||
height: 1.75rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dashboard-skeleton-bar--kiosk-meta {
|
||||
width: 88%;
|
||||
height: 0.72rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dashboard-skeleton-bar--kiosk-meta:last-child {
|
||||
width: 62%;
|
||||
}
|
||||
|
||||
/* Legacy grid wrapper (unused on recipes_selection; kept for compatibility) */
|
||||
|
||||
.kiosk-skeleton-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
min-height: 8rem;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-grid--exit {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* --- Unloading groups list --- */
|
||||
|
||||
.kiosk-skeleton-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: 6rem;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-groups--exit {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-group-item {
|
||||
background: var(--white, #fff);
|
||||
border: 1px solid var(--gray-200, #e5e7eb);
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-group-item:nth-child(1) { --skel-i: 0; }
|
||||
.kiosk-skeleton-group-item:nth-child(2) { --skel-i: 1; }
|
||||
.kiosk-skeleton-group-item:nth-child(3) { --skel-i: 2; }
|
||||
|
||||
.kiosk-skeleton-group-item .dashboard-skeleton-bar--title {
|
||||
width: 55%;
|
||||
height: 1.1rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-group-item .dashboard-skeleton-bar--meta {
|
||||
width: 40%;
|
||||
height: 0.85rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* --- Duplicate / weight panel --- */
|
||||
|
||||
.kiosk-skeleton-panel {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-panel--exit {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-weight-hero {
|
||||
width: min(280px, 70%);
|
||||
height: clamp(2.5rem, 8vmin, 4rem);
|
||||
margin: 0 auto 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-recipe-header {
|
||||
padding: clamp(10px, 2.5vmin, 16px) clamp(12px, 3vw, 20px);
|
||||
}
|
||||
|
||||
.kiosk-skeleton-recipe-header .dashboard-skeleton-bar--title {
|
||||
width: 65%;
|
||||
height: 1.25rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-recipe-header .dashboard-skeleton-bar--subtitle {
|
||||
width: 45%;
|
||||
height: 0.85rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-component-name {
|
||||
width: min(320px, 85%);
|
||||
height: 1.5rem;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
/* --- Ingredients table skeleton --- */
|
||||
|
||||
.kiosk-skeleton-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-height: 12rem;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-table--exit {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-table-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-table-row:nth-child(1) { --skel-i: 0; }
|
||||
.kiosk-skeleton-table-row:nth-child(2) { --skel-i: 1; }
|
||||
.kiosk-skeleton-table-row:nth-child(3) { --skel-i: 2; }
|
||||
.kiosk-skeleton-table-row:nth-child(4) { --skel-i: 3; }
|
||||
.kiosk-skeleton-table-row:nth-child(5) { --skel-i: 4; }
|
||||
.kiosk-skeleton-table-row:nth-child(6) { --skel-i: 5; }
|
||||
|
||||
.kiosk-skeleton-table-row .dashboard-skeleton-bar {
|
||||
margin-bottom: 0;
|
||||
height: 0.85rem;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-table-row .dashboard-skeleton-bar:first-child {
|
||||
flex: 2 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kiosk-skeleton-table-row .dashboard-skeleton-bar:last-child {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
max-width: 35%;
|
||||
}
|
||||
|
||||
/* --- Dark theme --- */
|
||||
|
||||
html[data-theme="dark"] .dispenser-card.kiosk-skeleton-card {
|
||||
background: #242424;
|
||||
border-color: #3a3a3a;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .kiosk-skeleton-group-item {
|
||||
background: #242424;
|
||||
border-color: #3a3a3a;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dispenser-grid.kiosk-skeleton-host--exit,
|
||||
.kiosk-skeleton-grid--exit,
|
||||
.kiosk-skeleton-groups--exit,
|
||||
.kiosk-skeleton-panel--exit,
|
||||
.kiosk-skeleton-table--exit {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
.wesp-logo-loader {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
background: #f6f6f4;
|
||||
transition: opacity 0.48s ease, visibility 0.48s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wesp-logo-loader--exiting {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.wesp-logo-loader--exiting .wesp-logo-loader__canvas {
|
||||
transform: scale(1.03);
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.wesp-logo-loader--hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wesp-logo-loader--embedded {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wesp-logo-loader--embedded.wesp-logo-loader--transparent,
|
||||
.wesp-logo-loader--plain.wesp-logo-loader--transparent {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.wesp-logo-loader--embedded .wesp-logo-loader__canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.wesp-logo-loader--embedded:not(.wesp-logo-loader--no-chrome) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wesp-logo-loader--embedded:not(.wesp-logo-loader--no-chrome) .wesp-logo-loader__canvas {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
inset: auto;
|
||||
}
|
||||
|
||||
.wesp-logo-loader--embedded:not(.wesp-logo-loader--no-chrome) .wesp-logo-loader__footer {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 12px 10px;
|
||||
}
|
||||
|
||||
.wesp-logo-loader--embedded:not(.wesp-logo-loader--no-chrome) .wesp-logo-loader__footer-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.wesp-logo-loader--plain {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.wesp-logo-loader--no-chrome .wesp-logo-loader__footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wesp-logo-loader__canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
transition: transform 0.52s ease, opacity 0.52s ease;
|
||||
transform-origin: 50% 44%;
|
||||
}
|
||||
|
||||
.wesp-logo-loader__footer {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 0 12px 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wesp-logo-loader__footer-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.wesp-logo-loader__label,
|
||||
.wesp-logo-loader__percent {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(26, 30, 28, 0.72);
|
||||
}
|
||||
|
||||
.wesp-logo-loader__percent {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.wesp-logo-loader__bar {
|
||||
position: relative;
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
background: rgba(26, 30, 28, 0.08);
|
||||
}
|
||||
|
||||
.wesp-logo-loader__bar-fill {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
width: 0;
|
||||
background: rgb(148, 152, 158);
|
||||
transition: width 0.08s linear, background 0.18s ease;
|
||||
}
|
||||
|
||||
.wesp-logo-loader__tagline {
|
||||
margin: 14px 0 0;
|
||||
padding: 0 8px;
|
||||
text-align: center;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.45;
|
||||
color: rgba(26, 30, 28, 0.52);
|
||||
min-height: 1.45em;
|
||||
transition: opacity 0.35s ease;
|
||||
}
|
||||
|
||||
.wesp-logo-loader__tagline:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wesp-logo-loader__tagline--fade {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wesp-logo-loader__bar-fill {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Demo page helpers */
|
||||
.wesp-logo-loader-demo {
|
||||
min-height: 100vh;
|
||||
background: #f6f6f4;
|
||||
color: rgba(26, 30, 28, 0.72);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.wesp-logo-loader-demo__panel {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 72px;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid rgba(26, 30, 28, 0.08);
|
||||
box-shadow: 0 8px 24px rgba(26, 30, 28, 0.06);
|
||||
}
|
||||
|
||||
.wesp-logo-loader-demo__btn {
|
||||
appearance: none;
|
||||
border: 1px solid rgba(26, 30, 28, 0.12);
|
||||
background: rgba(72, 129, 109, 0.12);
|
||||
color: #1a1e1c;
|
||||
border-radius: 8px;
|
||||
padding: 8px 14px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wesp-logo-loader-demo__btn:hover {
|
||||
background: rgba(72, 129, 109, 0.22);
|
||||
}
|
||||
|
||||
.wesp-logo-loader-demo__hint {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
opacity: 0.65;
|
||||
}
|
||||
@@ -283,8 +283,13 @@
|
||||
}
|
||||
|
||||
body.recipe-edit-mobile-open .mobile-navbar,
|
||||
body.recipe-edit-mobile-open .mobile-menu-overlay,
|
||||
body.recipe-edit-mobile-open .app-footer,
|
||||
body.recipe-edit-mobile-open .page-content {
|
||||
body.recipe-edit-mobile-open .page-content,
|
||||
body.recipe-edit-mobile-open .wesp-zt-notify-fab,
|
||||
body.ingredient-mobile-sheet-open .wesp-zt-notify-fab,
|
||||
body.unloading-group-mobile-sheet-open .wesp-zt-notify-fab,
|
||||
body.recipe-help-modal-open .wesp-zt-notify-fab {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -345,7 +350,14 @@
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
touch-action: pan-y;
|
||||
padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
|
||||
padding-bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
/* Nested section cards inherit global .card { height: 100% } and stretch to fill the scroll viewport */
|
||||
body.recipe-edit-mobile-open #recipeEdit .recipe-section-card,
|
||||
#recipeEdit.recipe-edit--mobile-active .recipe-section-card {
|
||||
height: auto !important;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
body.recipe-edit-mobile-open #recipeEdit #recipeForm > .card-header,
|
||||
@@ -435,12 +447,23 @@
|
||||
}
|
||||
|
||||
/* --- Card layout for edit data grids --- */
|
||||
/* !important: wesp-recipes-editor.css loads later and sets display:grid on __head/__row */
|
||||
#recipeEdit .zt-data-grid__scroll {
|
||||
overflow: visible;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
#recipeEdit .zt-data-grid {
|
||||
gap: 0 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
#recipeEdit .zt-data-grid__head {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#recipeEdit #ingredientsTableBody .ingredient-row,
|
||||
#recipeEdit #unloadingGroupsTableBody .unloading-group-row {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
#recipeEdit #ingredientsTable.ingredients-table--dm-unlocked .ingredient-detail-cell.dry-matter-per-head {
|
||||
|
||||
@@ -300,13 +300,12 @@ html[data-theme="dark"] .reports-page .reports-filter-select .form-select {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.reports-page .reports-export-dropdown__menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 0.35rem);
|
||||
z-index: 1200;
|
||||
.reports-page .reports-export-dropdown__menu,
|
||||
#reportsExportMenu.reports-export-dropdown__menu {
|
||||
position: fixed;
|
||||
z-index: 3600;
|
||||
min-width: 16.5rem;
|
||||
max-width: 19rem;
|
||||
max-width: min(19rem, calc(100vw - 1.5rem));
|
||||
padding: 0.35rem;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--zt-radius-md, 10px);
|
||||
@@ -314,6 +313,16 @@ html[data-theme="dark"] .reports-page .reports-filter-select .form-select {
|
||||
box-shadow: var(--shadow-soft, 0 8px 24px rgba(15, 23, 42, 0.12));
|
||||
}
|
||||
|
||||
#reportsExportMenu.reports-export-dropdown__menu[hidden] {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
#reportsExportMenu.reports-export-dropdown__menu--open {
|
||||
display: block !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.reports-page .reports-export-dropdown__group + .reports-export-dropdown__group {
|
||||
margin-top: 0.35rem;
|
||||
padding-top: 0.35rem;
|
||||
@@ -375,7 +384,8 @@ html[data-theme="dark"] .reports-page .reports-filter-select .form-select {
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .reports-page .reports-export-dropdown__menu {
|
||||
html[data-theme="dark"] .reports-page .reports-export-dropdown__menu,
|
||||
html[data-theme="dark"] #reportsExportMenu.reports-export-dropdown__menu {
|
||||
background: var(--dm-field, #2c3338);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
@@ -432,9 +442,10 @@ html[data-theme="dark"] .reports-page .reports-export-dropdown__menu {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reports-page .reports-drp__panel {
|
||||
.reports-page .reports-drp__panel,
|
||||
#reportsDateRangePanel.reports-drp__panel {
|
||||
position: fixed;
|
||||
z-index: 2500;
|
||||
z-index: 3600;
|
||||
width: min(19rem, calc(100vw - 1.5rem));
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-light);
|
||||
@@ -443,6 +454,16 @@ html[data-theme="dark"] .reports-page .reports-export-dropdown__menu {
|
||||
box-shadow: var(--shadow-soft, 0 8px 24px rgba(15, 23, 42, 0.12));
|
||||
}
|
||||
|
||||
#reportsDateRangePanel.reports-drp__panel[hidden] {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
#reportsDateRangePanel.reports-drp__panel--open {
|
||||
display: block !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.reports-page .reports-drp__presets {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
@@ -549,7 +570,8 @@ html[data-theme="dark"] .reports-page .reports-export-dropdown__menu {
|
||||
|
||||
|
||||
html[data-theme="dark"] .reports-page .reports-drp__trigger,
|
||||
html[data-theme="dark"] .reports-page .reports-drp__panel {
|
||||
html[data-theme="dark"] .reports-page .reports-drp__panel,
|
||||
html[data-theme="dark"] #reportsDateRangePanel.reports-drp__panel {
|
||||
background: var(--dm-field, var(--bg-white, #2c3338));
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
color: #f5f5f5;
|
||||
|
||||
@@ -0,0 +1,737 @@
|
||||
:root {
|
||||
--setup-page-bg: #fff;
|
||||
--setup-overlay-bg: #fff;
|
||||
--setup-surface: #fff;
|
||||
--setup-surface-muted: rgba(26, 30, 28, 0.04);
|
||||
--setup-border: rgba(26, 30, 28, 0.08);
|
||||
--setup-border-strong: rgba(26, 30, 28, 0.14);
|
||||
--setup-text: #1a1e1c;
|
||||
--setup-text-muted: rgba(26, 30, 28, 0.72);
|
||||
--setup-text-soft: rgba(26, 30, 28, 0.55);
|
||||
--setup-text-faint: rgba(26, 30, 28, 0.5);
|
||||
--setup-accent: #2c7be5;
|
||||
--setup-accent-soft: rgba(44, 123, 229, 0.08);
|
||||
--setup-accent-text: #1a5bb8;
|
||||
--setup-shadow: 0 16px 48px rgba(26, 30, 28, 0.08);
|
||||
--setup-busy-bg: rgba(255, 255, 255, 0.82);
|
||||
--setup-info-bg: rgba(44, 123, 229, 0.08);
|
||||
--setup-info-text: rgba(26, 30, 28, 0.78);
|
||||
--setup-warn-bg: #fff7e6;
|
||||
--setup-warn-border: #ffd591;
|
||||
--setup-warn-text: #874d00;
|
||||
--setup-error: #b42318;
|
||||
--setup-ok: #067647;
|
||||
--setup-loader-bg: #fff;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--setup-page-bg: #0f1114;
|
||||
--setup-overlay-bg: #0f1114;
|
||||
--setup-surface: #181b20;
|
||||
--setup-surface-muted: rgba(255, 255, 255, 0.05);
|
||||
--setup-border: rgba(255, 255, 255, 0.1);
|
||||
--setup-border-strong: rgba(255, 255, 255, 0.16);
|
||||
--setup-text: #eef0f3;
|
||||
--setup-text-muted: rgba(238, 240, 243, 0.78);
|
||||
--setup-text-soft: rgba(238, 240, 243, 0.58);
|
||||
--setup-text-faint: rgba(238, 240, 243, 0.45);
|
||||
--setup-accent: #4d93ef;
|
||||
--setup-accent-soft: rgba(77, 147, 239, 0.14);
|
||||
--setup-accent-text: #8eb8ff;
|
||||
--setup-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
|
||||
--setup-busy-bg: rgba(24, 27, 32, 0.88);
|
||||
--setup-info-bg: rgba(77, 147, 239, 0.12);
|
||||
--setup-info-text: rgba(238, 240, 243, 0.86);
|
||||
--setup-warn-bg: rgba(255, 169, 64, 0.12);
|
||||
--setup-warn-border: rgba(255, 169, 64, 0.35);
|
||||
--setup-warn-text: #ffd591;
|
||||
--setup-error: #ff8a80;
|
||||
--setup-ok: #6ee7a8;
|
||||
--setup-loader-bg: #181b20;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--setup-page-bg);
|
||||
color: var(--setup-text);
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.wesp-setup {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10000;
|
||||
background: var(--setup-overlay-bg);
|
||||
}
|
||||
|
||||
.wesp-setup__intro {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10001;
|
||||
width: min(520px, calc(100vw - 32px));
|
||||
height: min(240px, 36vh);
|
||||
background: transparent;
|
||||
transition: opacity 0.45s ease, visibility 0.45s;
|
||||
}
|
||||
|
||||
.wesp-setup__intro--done {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wesp-setup__intro .wesp-logo-loader,
|
||||
.wesp-setup__intro .wesp-logo-loader--embedded,
|
||||
.wesp-setup__intro .wesp-logo-loader--plain {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.wesp-setup__panel {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10002;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(520px, calc(100vw - 32px));
|
||||
max-height: min(85vh, 620px);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border-radius: 16px;
|
||||
background: var(--setup-surface);
|
||||
border: 1px solid var(--setup-border);
|
||||
box-shadow: var(--setup-shadow);
|
||||
color: var(--setup-text);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: opacity 0.35s ease 0.08s, visibility 0.35s ease 0.08s, background 0.2s ease, color 0.2s ease;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.wesp-setup__header {
|
||||
flex: 0 0 auto;
|
||||
padding: 22px 24px 0;
|
||||
}
|
||||
|
||||
.wesp-setup__content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 16px 24px 8px;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.wesp-setup__footer {
|
||||
flex: 0 0 auto;
|
||||
padding: 12px 24px calc(16px + env(safe-area-inset-bottom, 0px));
|
||||
border-top: 1px solid var(--setup-border);
|
||||
background: var(--setup-surface);
|
||||
}
|
||||
|
||||
.wesp-setup__panel--busy {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wesp-setup__busy {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
border-radius: 16px;
|
||||
background: var(--setup-busy-bg);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.wesp-setup__spinner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 2px solid var(--setup-border-strong);
|
||||
border-top-color: var(--setup-accent);
|
||||
border-radius: 50%;
|
||||
animation: wesp-setup-spin 0.75s linear infinite;
|
||||
}
|
||||
|
||||
.wesp-setup__busy-label {
|
||||
font-size: 13px;
|
||||
color: var(--setup-text-muted);
|
||||
}
|
||||
|
||||
@keyframes wesp-setup-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.wesp-setup__panel--hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wesp-setup__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 21px;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.25;
|
||||
color: var(--setup-text);
|
||||
}
|
||||
|
||||
.wesp-setup__lead {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
color: var(--setup-text-muted);
|
||||
}
|
||||
|
||||
.wesp-setup__step-indicator {
|
||||
margin-bottom: 10px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--setup-text-faint);
|
||||
}
|
||||
|
||||
.wesp-setup__field {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.wesp-setup__label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--setup-text);
|
||||
}
|
||||
|
||||
.wesp-setup__input,
|
||||
.wesp-setup__select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid var(--setup-border-strong);
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
background: var(--setup-surface);
|
||||
color: var(--setup-text);
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.wesp-setup__input:focus,
|
||||
.wesp-setup__select:focus {
|
||||
outline: none;
|
||||
border-color: var(--setup-accent);
|
||||
box-shadow: 0 0 0 3px var(--setup-accent-soft);
|
||||
}
|
||||
|
||||
.wesp-setup__input:-webkit-autofill,
|
||||
.wesp-setup__input:-webkit-autofill:hover,
|
||||
.wesp-setup__input:-webkit-autofill:focus,
|
||||
.wesp-setup__input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 1000px var(--setup-surface) inset !important;
|
||||
box-shadow: 0 0 0 1000px var(--setup-surface) inset !important;
|
||||
-webkit-text-fill-color: var(--setup-text) !important;
|
||||
caret-color: var(--setup-text);
|
||||
transition: background-color 600000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.wesp-setup__input:autofill {
|
||||
box-shadow: 0 0 0 1000px var(--setup-surface) inset !important;
|
||||
-webkit-text-fill-color: var(--setup-text) !important;
|
||||
caret-color: var(--setup-text);
|
||||
}
|
||||
|
||||
.wesp-setup__input--readonly {
|
||||
background: var(--setup-surface-muted);
|
||||
color: var(--setup-text-muted);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.wesp-setup__input-group {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wesp-setup__input-group .wesp-setup__input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.wesp-setup__input-suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--setup-border-strong);
|
||||
border-left: none;
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: var(--setup-surface-muted);
|
||||
color: var(--setup-text-soft);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.wesp-setup__checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.wesp-setup__role-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wesp-setup__role-grid--cards {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wesp-setup__role-btn {
|
||||
text-align: left;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--setup-border-strong);
|
||||
background: var(--setup-surface);
|
||||
color: var(--setup-text);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.wesp-setup__role-btn:hover {
|
||||
border-color: var(--setup-accent);
|
||||
}
|
||||
|
||||
.wesp-setup__role-btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--setup-accent-soft);
|
||||
}
|
||||
|
||||
.wesp-setup__role-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 84px;
|
||||
padding: 20px 18px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wesp-setup__theme-grid .wesp-setup__theme-card {
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0;
|
||||
min-height: 0;
|
||||
padding: 12px 14px 12px 62px;
|
||||
text-align: left;
|
||||
background: var(--setup-surface-muted);
|
||||
border-color: var(--setup-border);
|
||||
color: var(--setup-text);
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
.wesp-setup__theme-grid .wesp-setup__theme-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-top: -16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--setup-border-strong);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wesp-setup__theme-card--light::before {
|
||||
background: linear-gradient(145deg, #ffffff 0%, #d8dee8 100%);
|
||||
}
|
||||
|
||||
.wesp-setup__theme-card--dark::before {
|
||||
background: linear-gradient(145deg, #505662 0%, #171b21 100%);
|
||||
}
|
||||
|
||||
.wesp-setup__theme-card span {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
font-weight: 400;
|
||||
color: var(--setup-text-soft);
|
||||
}
|
||||
|
||||
.wesp-setup__role-btn strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wesp-setup__role-card strong {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
color: var(--setup-text);
|
||||
}
|
||||
|
||||
.wesp-setup__theme-card strong {
|
||||
margin-bottom: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.wesp-setup__role-btn.is-selected:not(.wesp-setup__theme-card) {
|
||||
border-color: var(--setup-accent);
|
||||
background: var(--setup-accent-soft);
|
||||
box-shadow: 0 0 0 1px rgba(44, 123, 229, 0.18);
|
||||
}
|
||||
|
||||
.wesp-setup__theme-card.is-selected {
|
||||
border-color: var(--setup-accent);
|
||||
background: var(--setup-accent-soft);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wesp-setup__role-card.is-selected:not(.wesp-setup__theme-card) strong {
|
||||
color: var(--setup-accent-text);
|
||||
}
|
||||
|
||||
.wesp-setup__theme-grid .wesp-setup__theme-card strong {
|
||||
color: var(--setup-text);
|
||||
}
|
||||
|
||||
.wesp-setup__theme-grid .wesp-setup__theme-card.is-selected strong {
|
||||
color: var(--setup-accent-text);
|
||||
}
|
||||
|
||||
.wesp-setup__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wesp-setup__actions-fill {
|
||||
flex: 1 1 12px;
|
||||
min-width: 8px;
|
||||
}
|
||||
|
||||
.wesp-setup__btn {
|
||||
appearance: none;
|
||||
border: 1px solid var(--setup-border-strong);
|
||||
border-radius: 10px;
|
||||
padding: 10px 16px;
|
||||
min-height: 42px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
background: var(--setup-surface);
|
||||
color: var(--setup-text);
|
||||
transition: background 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.wesp-setup__btn:hover:not(:disabled) {
|
||||
border-color: var(--setup-accent);
|
||||
}
|
||||
|
||||
.wesp-setup__btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--setup-accent-soft);
|
||||
}
|
||||
|
||||
.wesp-setup__btn--ghost {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--setup-text-muted);
|
||||
}
|
||||
|
||||
.wesp-setup__btn--ghost:hover:not(:disabled) {
|
||||
background: var(--setup-surface-muted);
|
||||
border-color: var(--setup-border);
|
||||
color: var(--setup-text);
|
||||
}
|
||||
|
||||
.wesp-setup__btn--primary {
|
||||
background: var(--setup-accent);
|
||||
border-color: var(--setup-accent);
|
||||
color: #fff;
|
||||
padding-inline: 20px;
|
||||
}
|
||||
|
||||
.wesp-setup__btn--primary:hover:not(:disabled) {
|
||||
filter: brightness(1.06);
|
||||
border-color: var(--setup-accent);
|
||||
}
|
||||
|
||||
.wesp-setup__btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wesp-setup__msg {
|
||||
margin: 10px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
min-height: 1.4em;
|
||||
}
|
||||
|
||||
.wesp-setup__msg--error {
|
||||
color: var(--setup-error);
|
||||
}
|
||||
|
||||
.wesp-setup__msg--ok {
|
||||
color: var(--setup-ok);
|
||||
}
|
||||
|
||||
.wesp-setup__hint {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--setup-text-soft);
|
||||
}
|
||||
|
||||
.wesp-setup__info {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: var(--setup-info-bg);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--setup-info-text);
|
||||
}
|
||||
|
||||
.wesp-setup__info--warn {
|
||||
background: var(--setup-warn-bg);
|
||||
border: 1px solid var(--setup-warn-border);
|
||||
color: var(--setup-warn-text);
|
||||
}
|
||||
|
||||
.wesp-setup__section-title {
|
||||
margin: 18px 0 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wesp-setup__qr-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 8px 0 10px;
|
||||
padding: 14px;
|
||||
border-radius: 12px;
|
||||
background: var(--setup-surface-muted);
|
||||
border: 1px solid var(--setup-border);
|
||||
}
|
||||
|
||||
.wesp-setup__qr {
|
||||
display: block;
|
||||
width: min(200px, 100%);
|
||||
height: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wesp-setup__url {
|
||||
word-break: break-all;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
color: var(--setup-text-muted);
|
||||
}
|
||||
|
||||
.wesp-setup__progress-bar {
|
||||
height: 6px;
|
||||
background: var(--setup-surface-muted);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.wesp-setup__progress-fill {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: var(--setup-accent);
|
||||
transition: width 0.2s linear;
|
||||
}
|
||||
|
||||
.wesp-setup__summary-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.wesp-setup__input--touch {
|
||||
cursor: text;
|
||||
caret-color: var(--setup-accent);
|
||||
}
|
||||
|
||||
.wesp-setup__keyboard {
|
||||
flex: 0 0 auto;
|
||||
display: none;
|
||||
padding: 10px 16px 4px;
|
||||
border-top: 1px solid var(--setup-border);
|
||||
background: var(--setup-surface);
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.wesp-setup__keyboard.is-visible {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.wesp-setup__keyboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.wesp-setup__keyboard-key {
|
||||
appearance: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 42px;
|
||||
padding: 8px 4px;
|
||||
border: 1px solid var(--setup-border-strong);
|
||||
border-radius: 8px;
|
||||
background: var(--setup-surface-muted);
|
||||
color: var(--setup-text);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: background 0.12s ease, border-color 0.12s ease, transform 0.08s ease;
|
||||
}
|
||||
|
||||
.wesp-setup__keyboard-key:active {
|
||||
transform: scale(0.96);
|
||||
background: var(--setup-accent-soft);
|
||||
border-color: var(--setup-accent);
|
||||
}
|
||||
|
||||
.wesp-setup__keyboard-key--wide {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.wesp-setup__keyboard-key--action {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.wesp-setup__intro {
|
||||
height: min(200px, 32vh);
|
||||
}
|
||||
|
||||
.wesp-setup__panel {
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
transform: none;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: min(92dvh, 720px);
|
||||
border-radius: 18px 18px 0 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.wesp-setup__header {
|
||||
padding: 18px 18px 0;
|
||||
}
|
||||
|
||||
.wesp-setup__content {
|
||||
padding: 14px 18px 6px;
|
||||
}
|
||||
|
||||
.wesp-setup__footer {
|
||||
padding: 10px 18px calc(14px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.wesp-setup__title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.wesp-setup__role-card {
|
||||
min-height: 72px;
|
||||
padding: 16px 14px;
|
||||
}
|
||||
|
||||
.wesp-setup__theme-grid .wesp-setup__theme-card {
|
||||
padding: 11px 12px 11px 56px;
|
||||
}
|
||||
|
||||
.wesp-setup__theme-grid .wesp-setup__theme-card::before {
|
||||
left: 12px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-top: -14px;
|
||||
}
|
||||
|
||||
.wesp-setup__btn {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.wesp-setup__actions {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wesp-setup__actions-fill {
|
||||
flex-basis: 100%;
|
||||
height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wesp-setup__btn--primary {
|
||||
flex: 1 1 auto;
|
||||
min-width: calc(50% - 4px);
|
||||
}
|
||||
|
||||
.wesp-setup__keyboard {
|
||||
padding: 8px 14px 2px;
|
||||
}
|
||||
|
||||
.wesp-setup__keyboard-grid {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.wesp-setup__keyboard-key {
|
||||
min-height: 44px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.wesp-setup__keyboard-key--action {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/* Экран /starting — тот же масштаб логотипа, что intro в setup wizard */
|
||||
.wesp-startup-screen {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background: #f6f6f4;
|
||||
color: rgba(26, 30, 28, 0.72);
|
||||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.wesp-startup__stage {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10001;
|
||||
width: min(520px, calc(100vw - 32px));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.wesp-startup__stage .wesp-setup__intro {
|
||||
position: relative;
|
||||
left: auto;
|
||||
top: auto;
|
||||
transform: none;
|
||||
width: 100%;
|
||||
height: min(240px, 36vh);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.wesp-startup__stage .wesp-logo-loader,
|
||||
.wesp-startup__stage .wesp-logo-loader--embedded,
|
||||
.wesp-startup__stage .wesp-logo-loader--plain {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Подпись только снаружи (#startupQuip), не пустой tagline внутри лоадера */
|
||||
.wesp-startup__stage .wesp-logo-loader__tagline {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.wesp-startup__quip {
|
||||
flex: 0 0 auto;
|
||||
margin: 10px 0 0;
|
||||
padding: 0 12px;
|
||||
text-align: center;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.5;
|
||||
color: rgba(26, 30, 28, 0.58);
|
||||
min-height: 3em;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.wesp-startup__quip--fade {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.wesp-startup__quip--error {
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
/* Киоск / логин зоотехника: --z-login-bg из zootechnician-login.css */
|
||||
html[data-theme="dark"] .wesp-startup-screen {
|
||||
background: #12181b;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .wesp-startup__quip {
|
||||
color: #a8b0b8;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .wesp-startup__quip--error {
|
||||
color: #ff7875;
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/* Ненавязчивый баннер «доступно обновление» — киоск и операторские страницы */
|
||||
|
||||
#wespUpdateBanner {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 4500;
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 14px;
|
||||
padding-bottom: max(10px, env(safe-area-inset-bottom, 0px));
|
||||
background: linear-gradient(180deg, rgba(30, 58, 138, 0.97), rgba(29, 78, 216, 0.98));
|
||||
color: #f8fafc;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
box-shadow: 0 -4px 24px rgba(15, 23, 42, 0.25);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
#wespUpdateBanner.wesp-update-banner--visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#wespUpdateBanner .wesp-update-banner-inner {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px 14px;
|
||||
}
|
||||
|
||||
#wespUpdateBanner .wesp-update-banner-text {
|
||||
flex: 1 1 200px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#wespUpdateBanner .wesp-update-banner-text strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#wespUpdateBanner .wesp-update-banner-changelog {
|
||||
display: none;
|
||||
width: 100%;
|
||||
margin: 4px 0 0;
|
||||
padding: 8px 10px;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 120px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#wespUpdateBanner.wesp-update-banner--expanded .wesp-update-banner-changelog {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#wespUpdateBanner .wesp-update-banner-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#wespUpdateBanner .wesp-update-banner-btn {
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
#wespUpdateBanner .wesp-update-banner-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
#wespUpdateBanner .wesp-update-banner-btn--primary {
|
||||
background: #fff;
|
||||
color: #1d4ed8;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
#wespUpdateBanner .wesp-update-banner-btn--primary:hover {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
#wespUpdateOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 6000;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: rgba(15, 23, 42, 0.72);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#wespUpdateOverlay.wesp-update-overlay--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#wespUpdateOverlay .wesp-update-overlay-panel {
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
border-radius: 10px;
|
||||
padding: 24px 28px;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: Inter, system-ui, sans-serif;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
#wespUpdateOverlay .wesp-update-overlay-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#wespUpdateOverlay .wesp-update-overlay-body {
|
||||
margin: 0 0 16px;
|
||||
font-size: 0.95rem;
|
||||
color: #4b5563;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#wespUpdateOverlay .wesp-update-progress-wrap {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#wespUpdateOverlay .wesp-update-progress-track {
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #e5e7eb;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#wespUpdateOverlay .wesp-update-progress-bar {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--primary-color, #2196f3),
|
||||
var(--primary-dark, #1976d2)
|
||||
);
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
|
||||
#wespUpdateOverlay .wesp-update-progress-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
font-size: 0.8125rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
#wespUpdateOverlay .wesp-update-progress-percent {
|
||||
font-weight: 600;
|
||||
color: #1d4ed8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#wespUpdateOverlay .wesp-update-progress-detail {
|
||||
text-align: right;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #wespUpdateOverlay .wesp-update-overlay-panel {
|
||||
background: #242424;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
#wespUpdateOverlay .wesp-update-progress-bar.wesp-update-progress-bar--error {
|
||||
background: linear-gradient(90deg, #dc2626, #b91c1c);
|
||||
}
|
||||
|
||||
#wespUpdateOverlay .wesp-update-overlay-panel--rollback {
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #wespUpdateOverlay .wesp-update-overlay-panel--rollback {
|
||||
border-color: #7f1d1d;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #wespUpdateOverlay .wesp-update-overlay-body {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #wespUpdateOverlay .wesp-update-progress-track {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #wespUpdateOverlay .wesp-update-progress-meta {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #wespUpdateOverlay .wesp-update-progress-percent {
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#wespUpdateBanner .wesp-update-banner-inner {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
#wespUpdateBanner .wesp-update-banner-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
#wespUpdateBanner .wesp-update-banner-btn {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -544,32 +544,44 @@ html[data-theme="dark"] .mobile-brand .logo--dark-theme {
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
z-index: 2000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--transition), visibility var(--transition);
|
||||
}
|
||||
|
||||
.mobile-menu-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -100%;
|
||||
right: 0;
|
||||
width: min(88vw, 340px);
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
height: 100dvh;
|
||||
background: var(--gradient-blue);
|
||||
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.25);
|
||||
transition: right 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.mobile-menu-overlay.active .mobile-menu {
|
||||
right: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
body.mobile-menu-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-menu-header {
|
||||
@@ -578,6 +590,7 @@ html[data-theme="dark"] .mobile-brand .logo--dark-theme {
|
||||
align-items: center;
|
||||
padding: 1.25rem 1.25rem 1rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-brand {
|
||||
@@ -609,9 +622,11 @@ html[data-theme="dark"] .mobile-brand .logo--dark-theme {
|
||||
}
|
||||
|
||||
.mobile-menu-items {
|
||||
flex: 1;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 0.5rem 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.mobile-menu-item {
|
||||
@@ -643,6 +658,7 @@ html[data-theme="dark"] .mobile-brand .logo--dark-theme {
|
||||
.mobile-menu-footer {
|
||||
padding: 1.25rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-user-info {
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
/* Страница входа зоотехника — тёмная тема по референсу, светлая — отдельная палитра */
|
||||
|
||||
:root {
|
||||
--z-login-bg: var(--bg-page, #f5f7fa);
|
||||
--z-login-surface: var(--surface, #ffffff);
|
||||
--z-login-input-bg: #e8edf2;
|
||||
--z-login-text: var(--text, #1e293b);
|
||||
--z-login-muted: var(--muted, #64748b);
|
||||
--z-login-icon: #4b5563;
|
||||
--z-login-btn: var(--primary-color, #2196f3);
|
||||
--z-login-btn-hover: var(--primary-dark, #1976d2);
|
||||
--z-login-shadow: 0 16px 48px rgba(17, 24, 39, 0.1),
|
||||
0 0 0 1px rgba(17, 24, 39, 0.06);
|
||||
--z-login-switch-track: #cbd5e1;
|
||||
--z-login-switch-thumb: #ffffff;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--z-login-bg: var(--bg-page, #12181b);
|
||||
--z-login-surface: var(--surface, #2c3338);
|
||||
--z-login-input-bg: var(--field, #3e444a);
|
||||
--z-login-text: var(--text, #f5f5f5);
|
||||
--z-login-muted: var(--muted, #a8b0b8);
|
||||
--z-login-icon: #ffffff;
|
||||
--z-login-btn: var(--primary-color, #2196f3);
|
||||
--z-login-btn-hover: var(--primary-light, #42a5f5);
|
||||
--z-login-shadow: 0 20px 56px rgba(0, 0, 0, 0.45),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04);
|
||||
--z-login-switch-track: #252b30;
|
||||
--z-login-switch-thumb: #ffffff;
|
||||
}
|
||||
|
||||
html[data-theme="organic"] {
|
||||
--z-login-bg: var(--bg-page, #f6f6f4);
|
||||
--z-login-surface: var(--surface, #ffffff);
|
||||
--z-login-input-bg: #eff4f2;
|
||||
--z-login-text: var(--text, #1a1e1c);
|
||||
--z-login-muted: var(--muted, #4a5a52);
|
||||
--z-login-icon: #4a5a52;
|
||||
--z-login-btn: var(--primary-color, #48816d);
|
||||
--z-login-btn-hover: var(--primary-dark, #3a6b58);
|
||||
--z-login-shadow: 0 16px 48px rgba(26, 30, 28, 0.08),
|
||||
0 0 0 1px rgba(26, 30, 28, 0.04);
|
||||
--z-login-switch-track: #e8e8e2;
|
||||
--z-login-switch-thumb: #ffffff;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body.zootech-login-body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: var(--zootech-font, "Inter", system-ui, sans-serif);
|
||||
background: var(--z-login-bg);
|
||||
color: var(--z-login-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.z-login-page {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
transition: opacity 0.25s ease, visibility 0.25s;
|
||||
}
|
||||
|
||||
.z-login-page--hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.z-login-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10000;
|
||||
background: var(--z-login-bg);
|
||||
}
|
||||
|
||||
.z-login-loader {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10001;
|
||||
width: min(520px, calc(100vw - 32px));
|
||||
height: min(240px, 36vh);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.z-login-loader .wesp-logo-loader,
|
||||
.z-login-loader .wesp-logo-loader--embedded,
|
||||
.z-login-loader .wesp-logo-loader--plain {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.z-login-loader .wesp-logo-loader__label,
|
||||
.z-login-loader .wesp-logo-loader__percent {
|
||||
color: var(--z-login-muted);
|
||||
}
|
||||
|
||||
.z-login-loader .wesp-logo-loader__bar {
|
||||
background: rgba(26, 30, 28, 0.1);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .z-login-loader .wesp-logo-loader__bar {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.z-login-card {
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--z-login-shadow);
|
||||
background: var(--z-login-surface);
|
||||
}
|
||||
|
||||
.z-login-form-panel {
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.z-login-title {
|
||||
font-family: inherit;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: var(--z-login-text);
|
||||
margin: 0 0 28px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.z-login-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.z-login-field--with-icon {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
background: var(--z-login-input-bg);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.z-login-field-icon {
|
||||
flex: 0 0 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--z-login-icon);
|
||||
}
|
||||
|
||||
.z-login-field-icon svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.z-login-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 14px 14px 14px 0;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--z-login-text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.z-login-input::placeholder {
|
||||
color: var(--z-login-muted);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.z-login-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.z-login-input:-webkit-autofill,
|
||||
.z-login-input:-webkit-autofill:hover,
|
||||
.z-login-input:-webkit-autofill:focus,
|
||||
.z-login-input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 1000px var(--z-login-input-bg) inset !important;
|
||||
box-shadow: 0 0 0 1000px var(--z-login-input-bg) inset !important;
|
||||
-webkit-text-fill-color: var(--z-login-text) !important;
|
||||
caret-color: var(--z-login-text);
|
||||
transition: background-color 600000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.z-login-input:autofill {
|
||||
box-shadow: 0 0 0 1000px var(--z-login-input-bg) inset !important;
|
||||
-webkit-text-fill-color: var(--z-login-text) !important;
|
||||
caret-color: var(--z-login-text);
|
||||
}
|
||||
|
||||
.z-login-field--with-icon:focus-within {
|
||||
box-shadow: 0 0 0 2px rgba(var(--primary-rgb, 33, 150, 243), 0.45);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .z-login-field--with-icon:focus-within {
|
||||
box-shadow: 0 0 0 2px rgba(var(--primary-rgb, 33, 150, 243), 0.5);
|
||||
}
|
||||
|
||||
.z-login-submit {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 14px 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
font-family: inherit;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: var(--gradient-blue, var(--z-login-btn));
|
||||
box-shadow: 0 4px 14px rgba(var(--primary-rgb, 33, 150, 243), 0.35);
|
||||
transition: background 0.15s ease, transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.z-login-submit:hover:not(:disabled) {
|
||||
background: var(--z-login-btn-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.z-login-submit:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.z-login-remember {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 22px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.z-login-switch-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.z-login-switch-ui {
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 26px;
|
||||
border-radius: 13px;
|
||||
background: var(--z-login-switch-track);
|
||||
position: relative;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.z-login-switch-ui::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: var(--z-login-switch-thumb);
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.z-login-switch-input:checked + .z-login-switch-ui::after {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
|
||||
.z-login-switch-input:focus-visible + .z-login-switch-ui {
|
||||
outline: 2px solid var(--z-login-btn);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.z-login-switch-input:disabled + .z-login-switch-ui {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.z-login-remember-text {
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
color: var(--z-login-text);
|
||||
}
|
||||
|
||||
.z-login-msg {
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.9rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.z-login-msg--error {
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.z-login-spinner {
|
||||
display: none;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin: 0 auto;
|
||||
border: 2px solid rgba(255, 255, 255, 0.35);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: z-login-spin 0.85s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes z-login-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.z-login-loader {
|
||||
height: min(200px, 32vh);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.z-login-form-panel {
|
||||
padding: 28px 22px 32px;
|
||||
}
|
||||
|
||||
.z-login-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 320 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Комптон">
|
||||
<text x="16" y="24.5" text-anchor="middle"
|
||||
font-family="Arial, Helvetica, 'DejaVu Sans', sans-serif"
|
||||
font-size="22" font-weight="700" fill="#ffffff">К</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 284 B |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 640 512" fill="currentColor" aria-hidden="true" focusable="false" role="img">
|
||||
<path d="M634 276.8l-9.999-13.88L624 185.7c0-11.88-12.5-19.49-23.12-14.11c-10.88 5.375-19.5 13.5-26.38 23l-65.75-90.92C490.6 78.71 461.8 64 431 64H112C63.37 64 24 103.4 24 152v86.38C9.5 250.1 0 267.9 0 288v32h8c35.38 0 64-28.62 64-64L72 152c0-16.88 10.5-31.12 25.38-37C96.5 119.1 96 123.5 96 128l.0002 304c0 8.875 7.126 16 16 16h63.1c8.875 0 16-7.125 16-16l.0006-112c9.375 9.375 20.25 16.5 32 21.88V368c0 8.875 7.252 16 16 16c8.875 0 15.1-7.125 15.1-16v-17.25c9.125 1 12.88 2.25 32-.125V368c0 8.875 7.25 16 16 16c8.875 0 16-7.125 16-16v-26.12C331.8 336.5 342.6 329.2 352 320l-.0012 112c0 8.875 7.125 16 15.1 16h64c8.75 0 16-7.125 16-16V256l31.1 32l.0006 41.55c0 12.62 3.752 24.95 10.75 35.45l41.25 62C540.8 440.1 555.5 448 571.4 448c22.5 0 41.88-15.88 46.25-38l21.75-108.6C641.1 292.8 639.1 283.9 634 276.8zM377.3 167.4l-22.88 22.75C332.5 211.8 302.9 224 272.1 224S211.5 211.8 189.6 190.1L166.8 167.4C151 151.8 164.4 128 188.9 128h166.2C379.6 128 393 151.8 377.3 167.4zM576 352c-8.875 0-16-7.125-16-16s7.125-16 16-16s16 7.125 16 16S584.9 352 576 352z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="5 2 14 20" fill="currentColor" aria-hidden="true" role="img">
|
||||
<path d="M12 22q-2.65 0-4.325-.837T6 19q0-.6.363-1.112T7.374 17l1.575 1.475q-.225.1-.488.225t-.412.3q.325.4 1.5.7T12 20t2.463-.3t1.512-.7q-.175-.2-.45-.325T15 18.45l1.55-1.5q.7.4 1.075.913T18 19q0 1.325-1.675 2.163T12 22m0-3q-3.525-2.6-5.262-5.05T5 9.15q0-1.775.638-3.113T7.275 3.8t2.25-1.35T12 2t2.475.45t2.25 1.35t1.638 2.238T19 9.15q0 2.35-1.737 4.8T12 19m0-8q.825 0 1.413-.587T14 9t-.587-1.412T12 7t-1.412.588T10 9t.588 1.413T12 11"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 524 B |
|
After Width: | Height: | Size: 39 KiB |
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* Модалка «Учётные документы» на /feed_consumption
|
||||
* Сценарий: Реквизиты → Подписи → Экспорт
|
||||
*/
|
||||
(function () {
|
||||
const ORG_FIELDS = [
|
||||
{ id: "faOrgName", key: "organization_name", label: "Организация" },
|
||||
{ id: "faOkpo", key: "okpo", label: "ОКПО" },
|
||||
{ id: "faDepartment", key: "department", label: "Отделение (участок)" },
|
||||
{ id: "faFarm", key: "farm_name", label: "Ферма" },
|
||||
{ id: "faBrigade", key: "brigade", label: "Бригада" },
|
||||
{ id: "faAnimalType", key: "animal_type", label: "Вид / группа скота" },
|
||||
{ id: "faResponsible", key: "responsible_person", label: "Ответственный" },
|
||||
{ id: "faZootech", key: "zootechnician", label: "Зоотехник (ФИО)" },
|
||||
{ id: "faWarehouse", key: "warehouse_keeper", label: "Кладовщик (ФИО)" },
|
||||
{ id: "faDocPrefix", key: "document_number_prefix", label: "Префикс номера документа" },
|
||||
];
|
||||
|
||||
const SIGNATURES = [
|
||||
{ role: "zootechnician", label: "Зоотехник", canvasId: "faSigZootech" },
|
||||
{ role: "warehouse_keeper", label: "Кладовщик", canvasId: "faSigWarehouse" },
|
||||
{ role: "recipient", label: "Получатель", canvasId: "faSigRecipient" },
|
||||
];
|
||||
|
||||
const canvases = {};
|
||||
let lastSignatureStatus = {};
|
||||
|
||||
function getExportMonth() {
|
||||
const monthEl = document.getElementById("faExportMonth");
|
||||
if (monthEl && monthEl.value) {
|
||||
return monthEl.value;
|
||||
}
|
||||
const fromEl = document.getElementById("consumptionDateFrom");
|
||||
if (fromEl && fromEl.value) {
|
||||
return fromEl.value.slice(0, 7);
|
||||
}
|
||||
const now = new Date();
|
||||
return now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0");
|
||||
}
|
||||
|
||||
function getDateRange() {
|
||||
const fromEl = document.getElementById("consumptionDateFrom");
|
||||
const toEl = document.getElementById("consumptionDateTo");
|
||||
let from = fromEl && fromEl.value;
|
||||
let to = toEl && toEl.value;
|
||||
if (!from || !to) {
|
||||
const now = new Date();
|
||||
from = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);
|
||||
to = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().slice(0, 10);
|
||||
}
|
||||
return { from, to, month: getExportMonth() };
|
||||
}
|
||||
|
||||
function syncExportMonthField() {
|
||||
const monthEl = document.getElementById("faExportMonth");
|
||||
if (!monthEl || monthEl.value) return;
|
||||
monthEl.value = getExportMonth();
|
||||
}
|
||||
|
||||
async function refreshSp20Status() {
|
||||
const el = document.getElementById("faSp20Status");
|
||||
if (!el) return;
|
||||
const month = getExportMonth();
|
||||
el.textContent = "Проверка данных за " + month + "…";
|
||||
try {
|
||||
const r = await fetch("/api/feed-accounting/sp20-preview?month=" + encodeURIComponent(month), {
|
||||
credentials: "same-origin",
|
||||
});
|
||||
if (!r.ok) {
|
||||
el.textContent = "Не удалось загрузить сводку по СП-20.";
|
||||
return;
|
||||
}
|
||||
const data = await r.json();
|
||||
if (!data.lines_count) {
|
||||
el.textContent = "За " + month + " нет строк расхода в отчётах загрузки. Проверьте месяц или наличие отчётов.";
|
||||
el.className = "text-warning small mb-3";
|
||||
return;
|
||||
}
|
||||
let msg = "СП-20 за " + month + ": " + data.lines_count + " строк, " + data.total_kg + " кг";
|
||||
if (!data.has_requisites) {
|
||||
msg += ". Заполните реквизиты на вкладке «Реквизиты СП-20».";
|
||||
el.className = "text-warning small mb-3";
|
||||
} else {
|
||||
el.className = "text-success small mb-3";
|
||||
}
|
||||
el.textContent = msg;
|
||||
} catch (err) {
|
||||
el.textContent = "Не удалось загрузить сводку по СП-20.";
|
||||
el.className = "text-danger small mb-3";
|
||||
}
|
||||
}
|
||||
|
||||
function userError(message, fallback) {
|
||||
const notifyApi = window.WespZootechNotify;
|
||||
if (notifyApi && typeof notifyApi.userFacingMessage === "function") {
|
||||
return notifyApi.userFacingMessage(message, fallback);
|
||||
}
|
||||
return fallback || "Не удалось выполнить операцию. Попробуйте ещё раз.";
|
||||
}
|
||||
|
||||
function notify(msg, type) {
|
||||
if (typeof notyf !== "undefined") {
|
||||
if (type === "error") notyf.error(msg);
|
||||
else notyf.success(msg);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadUrl(url, label) {
|
||||
const fallback = "Не удалось скачать документ. Попробуйте ещё раз.";
|
||||
try {
|
||||
const r = await fetch(url, { credentials: "same-origin" });
|
||||
if (!r.ok) {
|
||||
let body = null;
|
||||
try {
|
||||
body = await r.json();
|
||||
} catch (e) { /* ignore */ }
|
||||
const notifyApi = window.WespZootechNotify;
|
||||
const msg = notifyApi && typeof notifyApi.messageFromResponseBody === "function"
|
||||
? notifyApi.messageFromResponseBody(body, fallback)
|
||||
: userError(null, fallback);
|
||||
notify(msg, "error");
|
||||
return;
|
||||
}
|
||||
const blob = await r.blob();
|
||||
const cd = r.headers.get("Content-Disposition") || "";
|
||||
const m = cd.match(/filename="([^"]+)"/);
|
||||
const filename = m ? m[1] : "download";
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(function () { URL.revokeObjectURL(a.href); }, 5000);
|
||||
} catch (err) {
|
||||
notify(userError(null, fallback), "error");
|
||||
}
|
||||
}
|
||||
|
||||
function showTab(name) {
|
||||
document.querySelectorAll(".fa-tab-panel").forEach(function (el) {
|
||||
el.hidden = el.dataset.tab !== name;
|
||||
});
|
||||
document.querySelectorAll(".fa-tab-btn").forEach(function (btn) {
|
||||
btn.classList.toggle("active", btn.dataset.tab === name);
|
||||
});
|
||||
if (name === "export") {
|
||||
syncExportMonthField();
|
||||
refreshSp20Status();
|
||||
}
|
||||
}
|
||||
|
||||
function setupCanvas(canvas) {
|
||||
if (!canvas || canvases[canvas.id]) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.strokeStyle = "#1a1a1a";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = "round";
|
||||
let drawing = false;
|
||||
function pos(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
const ev = e.touches ? e.touches[0] : e;
|
||||
return {
|
||||
x: (ev.clientX - rect.left) * scaleX,
|
||||
y: (ev.clientY - rect.top) * scaleY,
|
||||
};
|
||||
}
|
||||
function start(e) {
|
||||
drawing = true;
|
||||
const p = pos(e);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
e.preventDefault();
|
||||
}
|
||||
function move(e) {
|
||||
if (!drawing) return;
|
||||
const p = pos(e);
|
||||
ctx.lineTo(p.x, p.y);
|
||||
ctx.stroke();
|
||||
e.preventDefault();
|
||||
}
|
||||
function end() { drawing = false; }
|
||||
canvas.addEventListener("mousedown", start);
|
||||
canvas.addEventListener("mousemove", move);
|
||||
canvas.addEventListener("mouseup", end);
|
||||
canvas.addEventListener("mouseleave", end);
|
||||
canvas.addEventListener("touchstart", start, { passive: false });
|
||||
canvas.addEventListener("touchmove", move, { passive: false });
|
||||
canvas.addEventListener("touchend", end);
|
||||
canvases[canvas.id] = { canvas: canvas, ctx: ctx };
|
||||
}
|
||||
|
||||
function clearCanvas(canvasId) {
|
||||
const item = canvases[canvasId];
|
||||
if (!item) return;
|
||||
item.ctx.clearRect(0, 0, item.canvas.width, item.canvas.height);
|
||||
}
|
||||
|
||||
function canvasHasInk(canvasId) {
|
||||
const item = canvases[canvasId];
|
||||
if (!item) return false;
|
||||
const data = item.ctx.getImageData(0, 0, item.canvas.width, item.canvas.height).data;
|
||||
for (let i = 3; i < data.length; i += 4) {
|
||||
if (data[i] > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function fillFarmDatalist(farms) {
|
||||
const list = document.getElementById("faFarmList");
|
||||
if (!list) return;
|
||||
list.innerHTML = "";
|
||||
(farms || []).forEach(function (farm) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = farm;
|
||||
list.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function isSetupComplete(settings, sigStatus) {
|
||||
const hasRequisites = Boolean(
|
||||
(settings.organization_name || "").trim() &&
|
||||
(settings.zootechnician || "").trim() &&
|
||||
(settings.warehouse_keeper || "").trim()
|
||||
);
|
||||
const hasSignatures = Boolean(sigStatus.zootechnician && sigStatus.warehouse_keeper);
|
||||
return hasRequisites && hasSignatures;
|
||||
}
|
||||
|
||||
async function loadOrgSettings() {
|
||||
const r = await fetch("/api/feed-accounting/org-settings", { credentials: "same-origin" });
|
||||
if (!r.ok) return null;
|
||||
const data = await r.json();
|
||||
ORG_FIELDS.forEach(function (f) {
|
||||
const el = document.getElementById(f.id);
|
||||
if (el) el.value = data[f.key] || "";
|
||||
});
|
||||
fillFarmDatalist(data.available_farms || []);
|
||||
if (data.signatures) {
|
||||
lastSignatureStatus = data.signatures;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function loadSignaturesOntoCanvases() {
|
||||
const roles = lastSignatureStatus;
|
||||
await Promise.all(
|
||||
SIGNATURES.map(function (s) {
|
||||
if (!roles[s.role]) return Promise.resolve();
|
||||
return new Promise(function (resolve) {
|
||||
const img = new Image();
|
||||
img.onload = function () {
|
||||
const item = canvases[s.canvasId];
|
||||
if (item) {
|
||||
item.ctx.clearRect(0, 0, item.canvas.width, item.canvas.height);
|
||||
item.ctx.drawImage(img, 0, 0, item.canvas.width, item.canvas.height);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
img.onerror = function () { resolve(); };
|
||||
img.src = "/api/feed-accounting/signatures/" + s.role + ".png?t=" + Date.now();
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function saveOrgSettings(silent) {
|
||||
const payload = {};
|
||||
ORG_FIELDS.forEach(function (f) {
|
||||
const el = document.getElementById(f.id);
|
||||
if (el) payload[f.key] = el.value.trim();
|
||||
});
|
||||
const r = await fetch("/api/feed-accounting/org-settings", {
|
||||
method: "PUT",
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok) {
|
||||
notify(userError(data.message, "Не удалось сохранить реквизиты"), "error");
|
||||
return false;
|
||||
}
|
||||
if (!silent) notify("Реквизиты сохранены");
|
||||
return true;
|
||||
}
|
||||
|
||||
async function saveSignature(role, canvasId, silent) {
|
||||
const item = canvases[canvasId];
|
||||
if (!item || !canvasHasInk(canvasId)) return true;
|
||||
const dataUrl = item.canvas.toDataURL("image/png");
|
||||
const r = await fetch("/api/feed-accounting/signatures/" + role, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ png_base64: dataUrl }),
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok) {
|
||||
notify(userError(data.message, "Не удалось сохранить подпись"), "error");
|
||||
return false;
|
||||
}
|
||||
if (data.roles) lastSignatureStatus = data.roles;
|
||||
if (!silent) notify("Подпись сохранена");
|
||||
return true;
|
||||
}
|
||||
|
||||
async function saveAll() {
|
||||
const btn = document.getElementById("faSaveAllBtn");
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
const orgOk = await saveOrgSettings(true);
|
||||
if (!orgOk) return;
|
||||
for (const s of SIGNATURES) {
|
||||
const ok = await saveSignature(s.role, s.canvasId, true);
|
||||
if (!ok) return;
|
||||
}
|
||||
notify("Реквизиты и подписи сохранены");
|
||||
const settings = await loadOrgSettings();
|
||||
if (settings && isSetupComplete(settings, lastSignatureStatus)) {
|
||||
showTab("export");
|
||||
}
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
window.openFeedAccountingModal = async function () {
|
||||
const modal = document.getElementById("feedAccountingModal");
|
||||
if (!modal) return;
|
||||
modal.style.display = "flex";
|
||||
document.body.style.overflow = "hidden";
|
||||
SIGNATURES.forEach(function (s) {
|
||||
setupCanvas(document.getElementById(s.canvasId));
|
||||
});
|
||||
const settings = await loadOrgSettings();
|
||||
await loadSignaturesOntoCanvases();
|
||||
syncExportMonthField();
|
||||
const complete = settings && isSetupComplete(settings, lastSignatureStatus);
|
||||
showTab(complete ? "export" : "requisites");
|
||||
};
|
||||
|
||||
window.closeFeedAccountingModal = function () {
|
||||
const modal = document.getElementById("feedAccountingModal");
|
||||
if (!modal) return;
|
||||
modal.style.display = "none";
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const openBtn = document.getElementById("openFeedAccountingBtn");
|
||||
if (openBtn) openBtn.addEventListener("click", function () { openFeedAccountingModal(); });
|
||||
|
||||
document.querySelectorAll(".fa-tab-btn").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
showTab(btn.dataset.tab);
|
||||
});
|
||||
});
|
||||
|
||||
const exportMonthEl = document.getElementById("faExportMonth");
|
||||
if (exportMonthEl) {
|
||||
exportMonthEl.addEventListener("change", refreshSp20Status);
|
||||
}
|
||||
|
||||
const saveAllBtn = document.getElementById("faSaveAllBtn");
|
||||
if (saveAllBtn) saveAllBtn.addEventListener("click", saveAll);
|
||||
|
||||
SIGNATURES.forEach(function (s) {
|
||||
const clearBtn = document.getElementById("clear_" + s.canvasId);
|
||||
if (clearBtn) clearBtn.addEventListener("click", function () { clearCanvas(s.canvasId); });
|
||||
});
|
||||
|
||||
const exports = [
|
||||
{ id: "faExportConsumptionXlsx", path: function (d) { return "/api/feed-accounting/consumption.xlsx?date_from=" + encodeURIComponent(d.from) + "&date_to=" + encodeURIComponent(d.to); }, label: "Потребление Excel" },
|
||||
{ id: "faExportConsumptionPdf", path: function (d) { return "/api/feed-accounting/consumption.pdf?date_from=" + encodeURIComponent(d.from) + "&date_to=" + encodeURIComponent(d.to); }, label: "Потребление PDF" },
|
||||
{ id: "faExportStock", path: function (d) { return "/api/feed-accounting/stock-balances.xlsx?date_from=" + encodeURIComponent(d.from) + "&date_to=" + encodeURIComponent(d.to); }, label: "Остатки" },
|
||||
{ id: "faExportSp20Xlsx", path: function (d) { return "/api/feed-accounting/sp20.xlsx?month=" + encodeURIComponent(d.month); }, label: "СП-20 Excel" },
|
||||
{ id: "faExportSp20Pdf", path: function (d) { return "/api/feed-accounting/sp20.pdf?month=" + encodeURIComponent(d.month); }, label: "СП-20 PDF" },
|
||||
{ id: "faExportJournalXlsx", path: function (d) { return "/api/feed-accounting/journal.xlsx?month=" + encodeURIComponent(d.month); }, label: "Журнал Excel" },
|
||||
{ id: "faExportJournalPdf", path: function (d) { return "/api/feed-accounting/journal.pdf?month=" + encodeURIComponent(d.month); }, label: "Журнал PDF" },
|
||||
{ id: "faExportZip", path: function (d) { return "/api/feed-accounting/documents.zip?date_from=" + encodeURIComponent(d.from) + "&date_to=" + encodeURIComponent(d.to) + "&month=" + encodeURIComponent(d.month); }, label: "ZIP" },
|
||||
];
|
||||
exports.forEach(function (ex) {
|
||||
const btn = document.getElementById(ex.id);
|
||||
if (!btn) return;
|
||||
btn.addEventListener("click", function () {
|
||||
const d = getDateRange();
|
||||
downloadUrl(ex.path(d), ex.label);
|
||||
});
|
||||
});
|
||||
|
||||
const modal = document.getElementById("feedAccountingModal");
|
||||
if (modal) {
|
||||
modal.addEventListener("click", function (e) {
|
||||
if (e.target === modal) closeFeedAccountingModal();
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Модальное окно «Калибровка»: iframe с /calibration?embed=1.
|
||||
*/
|
||||
(function () {
|
||||
const LOG = "[WespKioskCalibration]";
|
||||
const MENU_OVERLAY_ID = "wespKioskMenuOverlay";
|
||||
|
||||
function ensureStylesheet() {
|
||||
if (document.getElementById("kioskCalibrationModalCss")) return;
|
||||
const link = document.createElement("link");
|
||||
link.id = "kioskCalibrationModalCss";
|
||||
link.rel = "stylesheet";
|
||||
link.href = "/static/css/kiosk-calibration-modal.css";
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
function resizeFrame(iframe) {
|
||||
if (!iframe || !iframe.contentDocument) return;
|
||||
try {
|
||||
const doc = iframe.contentDocument;
|
||||
const height = Math.max(
|
||||
doc.body?.scrollHeight || 0,
|
||||
doc.documentElement?.scrollHeight || 0,
|
||||
320,
|
||||
);
|
||||
const max = Math.floor(window.innerHeight * 0.82) - 64;
|
||||
iframe.style.height = `${Math.min(height + 4, max)}px`;
|
||||
} catch (e) {
|
||||
console.warn(LOG, "iframe resize", e);
|
||||
}
|
||||
}
|
||||
|
||||
function createModal(options) {
|
||||
ensureStylesheet();
|
||||
const opts = options || {};
|
||||
const zIndex = Number(opts.zIndex) > 0 ? Number(opts.zIndex) : 5000;
|
||||
const light = opts.theme === "light";
|
||||
const panelTheme = light ? "light" : "dark";
|
||||
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "kioskCalibrationModal";
|
||||
modal.className = "kiosk-calibration-backdrop";
|
||||
modal.setAttribute("aria-hidden", "true");
|
||||
modal.style.zIndex = String(zIndex);
|
||||
modal.innerHTML = `
|
||||
<div class="kiosk-calibration-panel kiosk-calibration-panel--${panelTheme}" role="dialog" aria-modal="true" aria-labelledby="kioskCalibrationTitle">
|
||||
<div class="kiosk-calibration-head">
|
||||
<h2 class="kiosk-calibration-title" id="kioskCalibrationTitle">Калибровка весов</h2>
|
||||
<button type="button" class="kiosk-calibration-close" id="kioskCalibrationClose">Закрыть</button>
|
||||
</div>
|
||||
<iframe
|
||||
id="kioskCalibrationFrame"
|
||||
class="kiosk-calibration-frame"
|
||||
title="Калибровка весов"
|
||||
></iframe>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const iframe = modal.querySelector("#kioskCalibrationFrame");
|
||||
const closeBtn = modal.querySelector("#kioskCalibrationClose");
|
||||
const panel = modal.querySelector(".kiosk-calibration-panel");
|
||||
|
||||
function closeModal() {
|
||||
modal.classList.remove("is-open");
|
||||
modal.setAttribute("aria-hidden", "true");
|
||||
try {
|
||||
iframe.src = "about:blank";
|
||||
iframe.style.height = "";
|
||||
} catch (e) {
|
||||
console.warn(LOG, "iframe reset", e);
|
||||
}
|
||||
document.removeEventListener("keydown", onKey);
|
||||
}
|
||||
|
||||
function onKey(e) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
const menu = document.getElementById(MENU_OVERLAY_ID);
|
||||
if (menu) menu.style.display = "none";
|
||||
|
||||
iframe.onload = function () {
|
||||
window.setTimeout(function () {
|
||||
resizeFrame(iframe);
|
||||
}, 50);
|
||||
};
|
||||
iframe.src = "/calibration?embed=1";
|
||||
modal.classList.add("is-open");
|
||||
modal.setAttribute("aria-hidden", "false");
|
||||
document.addEventListener("keydown", onKey);
|
||||
}
|
||||
|
||||
closeBtn.addEventListener("click", closeModal);
|
||||
modal.addEventListener("click", function (e) {
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
if (panel) {
|
||||
panel.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
return { modal, iframe, openModal, closeModal, resizeFrame };
|
||||
}
|
||||
|
||||
function ensureModal(options) {
|
||||
const opts = options || {};
|
||||
if (!window.__wespKioskCalibrationApi) {
|
||||
window.__wespKioskCalibrationApi = createModal(opts);
|
||||
} else if (Number(opts.zIndex) > 0) {
|
||||
window.__wespKioskCalibrationApi.modal.style.zIndex = String(opts.zIndex);
|
||||
}
|
||||
return window.__wespKioskCalibrationApi;
|
||||
}
|
||||
|
||||
function openModal(options) {
|
||||
const api = ensureModal(options);
|
||||
const panel = api.modal.querySelector(".kiosk-calibration-panel");
|
||||
if (panel && options && options.theme) {
|
||||
const dark = options.theme === "dark";
|
||||
panel.classList.toggle("kiosk-calibration-panel--dark", dark);
|
||||
panel.classList.toggle("kiosk-calibration-panel--light", !dark);
|
||||
}
|
||||
api.openModal();
|
||||
return api;
|
||||
}
|
||||
|
||||
function setup(opts) {
|
||||
const options = opts || {};
|
||||
const linkSel = options.menuLinkSelector || "#calibrationLink";
|
||||
|
||||
if (typeof window.WespKioskMenu !== "undefined" && typeof window.WespKioskMenu.ensureOverlay === "function") {
|
||||
window.WespKioskMenu.ensureOverlay();
|
||||
}
|
||||
|
||||
const link = document.querySelector(linkSel);
|
||||
if (!link) {
|
||||
console.warn(LOG, "ссылка меню не найдена после ensureOverlay:", linkSel);
|
||||
return {};
|
||||
}
|
||||
|
||||
const api = ensureModal(options);
|
||||
|
||||
if (link.dataset.wespCalibrationBound === "1") {
|
||||
return { openModal: api.openModal, closeModal: api.closeModal };
|
||||
}
|
||||
link.dataset.wespCalibrationBound = "1";
|
||||
|
||||
link.addEventListener(
|
||||
"click",
|
||||
function (e) {
|
||||
if (e.ctrlKey || e.metaKey || e.shiftKey || e.button !== 0) return;
|
||||
if (window.frameElement) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
api.openModal();
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
return { openModal: api.openModal, closeModal: api.closeModal };
|
||||
}
|
||||
|
||||
window.WespKioskCalibration = { setup, ensureModal, openModal };
|
||||
})();
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Модальное окно «Проверка БД» в стиле киоска (как привязка терминала):
|
||||
* тёмная рамка, iframe с /db_check?embed=1.
|
||||
*/
|
||||
(function () {
|
||||
const LOG = "[WespKioskDbCheck]";
|
||||
const MENU_OVERLAY_ID = "wespKioskMenuOverlay";
|
||||
|
||||
function createModal() {
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "kioskDbCheckModal";
|
||||
modal.setAttribute("aria-hidden", "true");
|
||||
modal.style.cssText =
|
||||
"display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:5000;" +
|
||||
"align-items:center;justify-content:center;padding:12px;box-sizing:border-box;";
|
||||
modal.innerHTML = `
|
||||
<div class="kiosk-db-check-panel" style="
|
||||
background:#1f1f1f;
|
||||
color:#fff;
|
||||
border-radius:14px;
|
||||
border:1px solid #2f2f2f;
|
||||
box-shadow:0 0 20px rgba(0,0,0,.45);
|
||||
width:min(920px,100%);
|
||||
max-height:min(92vh,900px);
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
overflow:hidden;
|
||||
">
|
||||
<div style="
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
padding:14px 16px;
|
||||
border-bottom:1px solid #2f2f2f;
|
||||
flex-shrink:0;
|
||||
">
|
||||
<h2 style="margin:0;font-size:1.125rem;font-weight:600;line-height:1.3">Проверка баз данных</h2>
|
||||
<button type="button" id="kioskDbCheckClose" style="
|
||||
padding:10px 16px;
|
||||
border-radius:8px;
|
||||
border:1px solid #7a7a7a;
|
||||
background:#3a3a3a;
|
||||
color:#fff;
|
||||
cursor:pointer;
|
||||
font-size:15px;
|
||||
flex-shrink:0;
|
||||
">Закрыть</button>
|
||||
</div>
|
||||
<iframe
|
||||
id="kioskDbCheckFrame"
|
||||
title="Проверка баз данных"
|
||||
style="border:0;width:100%;flex:1;min-height:min(70vh,560px);background:#f9fafb;"
|
||||
></iframe>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const iframe = modal.querySelector("#kioskDbCheckFrame");
|
||||
const closeBtn = modal.querySelector("#kioskDbCheckClose");
|
||||
const panel = modal.querySelector(".kiosk-db-check-panel");
|
||||
|
||||
function closeModal() {
|
||||
modal.style.display = "none";
|
||||
modal.setAttribute("aria-hidden", "true");
|
||||
try {
|
||||
iframe.src = "about:blank";
|
||||
} catch (e) {
|
||||
console.warn(LOG, "iframe reset", e);
|
||||
}
|
||||
document.removeEventListener("keydown", onKey);
|
||||
}
|
||||
|
||||
function onKey(e) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
const menu = document.getElementById(MENU_OVERLAY_ID);
|
||||
if (menu) menu.style.display = "none";
|
||||
|
||||
iframe.src = "/db_check?embed=1";
|
||||
modal.style.display = "flex";
|
||||
modal.setAttribute("aria-hidden", "false");
|
||||
document.addEventListener("keydown", onKey);
|
||||
}
|
||||
|
||||
closeBtn.addEventListener("click", closeModal);
|
||||
modal.addEventListener("click", function (e) {
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
if (panel) {
|
||||
panel.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
return { modal, openModal, closeModal };
|
||||
}
|
||||
|
||||
function setup(opts) {
|
||||
const options = opts || {};
|
||||
const linkSel = options.menuLinkSelector || "#dbCheckLink";
|
||||
|
||||
if (typeof window.WespKioskMenu !== "undefined" && typeof window.WespKioskMenu.ensureOverlay === "function") {
|
||||
window.WespKioskMenu.ensureOverlay();
|
||||
}
|
||||
|
||||
const link = document.querySelector(linkSel);
|
||||
if (!link) {
|
||||
console.warn(LOG, "ссылка меню не найдена после ensureOverlay:", linkSel);
|
||||
return {};
|
||||
}
|
||||
|
||||
let api;
|
||||
if (!window.__wespKioskDbCheckApi) {
|
||||
api = createModal();
|
||||
window.__wespKioskDbCheckApi = api;
|
||||
} else {
|
||||
api = window.__wespKioskDbCheckApi;
|
||||
}
|
||||
|
||||
if (link.dataset.wespDbCheckBound === "1") {
|
||||
return { openModal: api.openModal, closeModal: api.closeModal };
|
||||
}
|
||||
link.dataset.wespDbCheckBound = "1";
|
||||
|
||||
link.addEventListener(
|
||||
"click",
|
||||
function (e) {
|
||||
if (e.ctrlKey || e.metaKey || e.shiftKey || e.button !== 0) return;
|
||||
if (window.frameElement) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
api.openModal();
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
return { openModal: api.openModal, closeModal: api.closeModal };
|
||||
}
|
||||
|
||||
window.WespKioskDbCheck = { setup };
|
||||
})();
|
||||
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* Диалоги для страниц киоска / разгрузки / отбора (терминал). Не подключать на зоотех-оболочку — там wesp-dialog.js (WespDialog).
|
||||
* API: WespKioskDialog.alert(msg, opts?) -> Promise<void>
|
||||
* WespKioskDialog.confirm(msg, opts?) -> Promise<boolean>
|
||||
* opts.timerOnly — закрытие только через WespKioskDialog.closeActive() (таймер); без кнопки, без Escape и клика по фону.
|
||||
*/
|
||||
(function (global) {
|
||||
var OVERLAY_ID = "wespKioskDialogOverlay";
|
||||
var STYLE_ID = "wesp-kiosk-dialog-styles";
|
||||
|
||||
function injectStyles() {
|
||||
if (document.getElementById(STYLE_ID)) return;
|
||||
var s = document.createElement("style");
|
||||
s.id = STYLE_ID;
|
||||
s.textContent =
|
||||
"#" +
|
||||
OVERLAY_ID +
|
||||
"{" +
|
||||
"position:fixed;inset:0;z-index:5000;" +
|
||||
"display:none;align-items:center;justify-content:center;" +
|
||||
"padding:16px;background:rgba(0,0,0,.55);" +
|
||||
"box-sizing:border-box;" +
|
||||
"}" +
|
||||
"#" +
|
||||
OVERLAY_ID +
|
||||
'[aria-hidden="false"]{display:flex;}' +
|
||||
".wesp-kiosk-dialog{" +
|
||||
"width:min(420px,100%);max-height:min(80vh,520px);overflow:auto;" +
|
||||
"background:#fff;color:#111827;border-radius:8px;" +
|
||||
"border:1px solid #e5e7eb;box-shadow:0 8px 32px rgba(0,0,0,.18);" +
|
||||
"padding:20px 22px;font-family:Inter,system-ui,sans-serif;" +
|
||||
"text-align:left;" +
|
||||
"}" +
|
||||
".wesp-kiosk-dialog-title{" +
|
||||
"margin:0 0 12px 0;font-size:1.125rem;font-weight:600;color:#111827;" +
|
||||
"}" +
|
||||
".wesp-kiosk-dialog-body{" +
|
||||
"margin:0;font-size:0.95rem;line-height:1.5;color:#374151;white-space:pre-wrap;word-break:break-word;" +
|
||||
"}" +
|
||||
".wesp-kiosk-dialog-actions{" +
|
||||
"display:flex;justify-content:flex-end;flex-wrap:wrap;gap:10px;margin-top:20px;" +
|
||||
"}" +
|
||||
".wesp-kiosk-dialog-btn{" +
|
||||
"min-width:120px;padding:10px 18px;border-radius:6px;font-size:0.95rem;font-weight:500;" +
|
||||
"cursor:pointer;font-family:inherit;border:1px solid #e5e7eb;background:#fff;color:#374151;" +
|
||||
"transition:background .2s,border-color .2s,box-shadow .2s;" +
|
||||
"}" +
|
||||
".wesp-kiosk-dialog-btn:hover{filter:brightness(.97);}" +
|
||||
".wesp-kiosk-dialog-btn-primary{" +
|
||||
"background:#2563eb;border-color:#2563eb;color:#fff;" +
|
||||
"}" +
|
||||
".wesp-kiosk-dialog-btn-primary:hover{box-shadow:0 4px 12px rgba(37,99,235,.25);}" +
|
||||
".wesp-kiosk-dialog-btn-danger{" +
|
||||
"background:#ef4444;border-color:#ef4444;color:#fff;" +
|
||||
"}" +
|
||||
".wesp-kiosk-dialog--danger{border-top:4px solid #ef4444;}" +
|
||||
".wesp-kiosk-dialog--success{border-top:4px solid #10b981;}" +
|
||||
"html[data-theme=dark] #" +
|
||||
OVERLAY_ID +
|
||||
" .wesp-kiosk-dialog{" +
|
||||
"background:#242424;color:#e6e6e6;border-color:#3a3a3a;" +
|
||||
"}" +
|
||||
"html[data-theme=dark] #" +
|
||||
OVERLAY_ID +
|
||||
" .wesp-kiosk-dialog-title{color:#f3f4f6;}" +
|
||||
"html[data-theme=dark] #" +
|
||||
OVERLAY_ID +
|
||||
" .wesp-kiosk-dialog-body{color:#d1d5db;}" +
|
||||
"html[data-theme=dark] #" +
|
||||
OVERLAY_ID +
|
||||
" .wesp-kiosk-dialog-btn{" +
|
||||
"background:#2d2d2d;border-color:#3a3a3a;color:#e6e6e6;" +
|
||||
"}" +
|
||||
"html[data-theme=dark] #" +
|
||||
OVERLAY_ID +
|
||||
" .wesp-kiosk-dialog-btn-primary{background:#2563eb;border-color:#2563eb;color:#fff;}" +
|
||||
"html[data-theme=dark] #" +
|
||||
OVERLAY_ID +
|
||||
" .wesp-kiosk-dialog-btn-danger{background:#dc2626;border-color:#dc2626;color:#fff;}" +
|
||||
"";
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function ensureDom() {
|
||||
injectStyles();
|
||||
var el = document.getElementById(OVERLAY_ID);
|
||||
if (el) return el;
|
||||
el = document.createElement("div");
|
||||
el.id = OVERLAY_ID;
|
||||
el.setAttribute("aria-hidden", "true");
|
||||
el.innerHTML =
|
||||
'<div class="wesp-kiosk-dialog" role="dialog" aria-modal="true" aria-labelledby="wespKioskDialogTitle">' +
|
||||
'<h2 class="wesp-kiosk-dialog-title" id="wespKioskDialogTitle"></h2>' +
|
||||
'<p class="wesp-kiosk-dialog-body" id="wespKioskDialogBody"></p>' +
|
||||
'<div class="wesp-kiosk-dialog-actions" id="wespKioskDialogActions"></div>' +
|
||||
"</div>";
|
||||
el.addEventListener("click", function (e) {
|
||||
if (e.target === el) {
|
||||
backdropDismiss();
|
||||
}
|
||||
});
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
var pendingResolve = null;
|
||||
var pendingMode = null;
|
||||
/** true: alert с opts.timerOnly — пользователь не закрывает окно вручную */
|
||||
var blockUserDismiss = false;
|
||||
|
||||
function backdropDismiss() {
|
||||
if (blockUserDismiss) return;
|
||||
if (pendingMode === "confirm" && pendingResolve) {
|
||||
var fn = pendingResolve;
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
fn(false);
|
||||
} else if (pendingMode === "alert" && pendingResolve) {
|
||||
var fn2 = pendingResolve;
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
fn2();
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
var el = document.getElementById(OVERLAY_ID);
|
||||
if (!el) return;
|
||||
var dlg = el.querySelector(".wesp-kiosk-dialog");
|
||||
if (dlg) dlg.removeAttribute("tabindex");
|
||||
blockUserDismiss = false;
|
||||
el.setAttribute("aria-hidden", "true");
|
||||
document.removeEventListener("keydown", onKey);
|
||||
}
|
||||
|
||||
/** Закрыть активный alert/confirm без клика (таймер, программное закрытие). */
|
||||
function closeActive() {
|
||||
if (pendingMode === "confirm" && pendingResolve) {
|
||||
var fnC = pendingResolve;
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
fnC(false);
|
||||
} else if (pendingMode === "alert" && pendingResolve) {
|
||||
var fnA = pendingResolve;
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
fnA();
|
||||
}
|
||||
}
|
||||
|
||||
function onKey(e) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
if (blockUserDismiss) return;
|
||||
backdropDismiss();
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
var el = document.getElementById(OVERLAY_ID);
|
||||
if (!el) return;
|
||||
el.setAttribute("aria-hidden", "false");
|
||||
document.addEventListener("keydown", onKey);
|
||||
}
|
||||
|
||||
function setDialogVariant(dialogEl, variant) {
|
||||
dialogEl.classList.remove("wesp-kiosk-dialog--danger", "wesp-kiosk-dialog--success");
|
||||
if (variant === "danger") dialogEl.classList.add("wesp-kiosk-dialog--danger");
|
||||
else if (variant === "success") dialogEl.classList.add("wesp-kiosk-dialog--success");
|
||||
}
|
||||
|
||||
function displayDialogMessage(message, opts) {
|
||||
const variant = opts && opts.variant;
|
||||
const fallback = opts && opts.fallback;
|
||||
if (variant === "danger" && global.WespUserMessages?.userFacingMessage) {
|
||||
return global.WespUserMessages.userFacingMessage(message, fallback || "Не удалось выполнить операцию. Попробуйте ещё раз.");
|
||||
}
|
||||
return message == null ? "" : String(message);
|
||||
}
|
||||
|
||||
function alert(message, opts) {
|
||||
opts = opts || {};
|
||||
return new Promise(function (resolve) {
|
||||
var overlay = ensureDom();
|
||||
var dialog = overlay.querySelector(".wesp-kiosk-dialog");
|
||||
var titleEl = document.getElementById("wespKioskDialogTitle");
|
||||
var bodyEl = document.getElementById("wespKioskDialogBody");
|
||||
var actions = document.getElementById("wespKioskDialogActions");
|
||||
titleEl.textContent = opts.title != null ? String(opts.title) : "Сообщение";
|
||||
bodyEl.textContent = displayDialogMessage(message, opts);
|
||||
setDialogVariant(dialog, opts.variant || "info");
|
||||
actions.innerHTML = "";
|
||||
blockUserDismiss = !!opts.timerOnly;
|
||||
pendingMode = "alert";
|
||||
pendingResolve = resolve;
|
||||
if (!opts.timerOnly) {
|
||||
var ok = document.createElement("button");
|
||||
ok.type = "button";
|
||||
ok.className = "wesp-kiosk-dialog-btn wesp-kiosk-dialog-btn-primary";
|
||||
ok.textContent = opts.okText || "Понятно";
|
||||
ok.addEventListener("click", function () {
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
resolve();
|
||||
});
|
||||
actions.appendChild(ok);
|
||||
show();
|
||||
ok.focus();
|
||||
} else {
|
||||
dialog.setAttribute("tabindex", "-1");
|
||||
show();
|
||||
dialog.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirm(message, opts) {
|
||||
opts = opts || {};
|
||||
return new Promise(function (resolve) {
|
||||
blockUserDismiss = false;
|
||||
var overlay = ensureDom();
|
||||
var dialog = overlay.querySelector(".wesp-kiosk-dialog");
|
||||
var titleEl = document.getElementById("wespKioskDialogTitle");
|
||||
var bodyEl = document.getElementById("wespKioskDialogBody");
|
||||
var actions = document.getElementById("wespKioskDialogActions");
|
||||
titleEl.textContent = opts.title != null ? String(opts.title) : "Подтверждение";
|
||||
bodyEl.textContent = message == null ? "" : String(message);
|
||||
setDialogVariant(dialog, opts.variant || "info");
|
||||
actions.innerHTML = "";
|
||||
var cancel = document.createElement("button");
|
||||
cancel.type = "button";
|
||||
cancel.className = "wesp-kiosk-dialog-btn";
|
||||
cancel.textContent = opts.cancelText || "Отмена";
|
||||
var ok = document.createElement("button");
|
||||
ok.type = "button";
|
||||
ok.className =
|
||||
"wesp-kiosk-dialog-btn " +
|
||||
(opts.danger ? "wesp-kiosk-dialog-btn-danger" : "wesp-kiosk-dialog-btn-primary");
|
||||
ok.textContent = opts.okText || "Да";
|
||||
cancel.addEventListener("click", function () {
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
resolve(false);
|
||||
});
|
||||
ok.addEventListener("click", function () {
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
resolve(true);
|
||||
});
|
||||
actions.appendChild(cancel);
|
||||
actions.appendChild(ok);
|
||||
pendingMode = "confirm";
|
||||
pendingResolve = resolve;
|
||||
show();
|
||||
ok.focus();
|
||||
});
|
||||
}
|
||||
|
||||
global.WespKioskDialog = {
|
||||
alert: alert,
|
||||
confirm: confirm,
|
||||
closeActive: closeActive,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
@@ -0,0 +1,169 @@
|
||||
(function () {
|
||||
const STYLE_ID = "wespKioskMenuStyles";
|
||||
const OVERLAY_ID = "wespKioskMenuOverlay";
|
||||
|
||||
function ensureStyles() {
|
||||
if (document.getElementById(STYLE_ID)) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = STYLE_ID;
|
||||
style.textContent = `
|
||||
#${OVERLAY_ID} {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.62);
|
||||
z-index: 4300;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
#${OVERLAY_ID} .wesp-kiosk-menu-panel {
|
||||
width: min(420px, 96vw);
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2f2f2f;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 0 24px rgba(0, 0, 0, 0.45);
|
||||
color: #fff;
|
||||
padding: 16px;
|
||||
}
|
||||
#${OVERLAY_ID} .wesp-kiosk-menu-logo-wrap {
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
#${OVERLAY_ID} .wesp-kiosk-menu-logo {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
}
|
||||
#${OVERLAY_ID} .wesp-kiosk-menu-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
#${OVERLAY_ID} .wesp-kiosk-menu-link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
background: #2b2b2b;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 15px;
|
||||
}
|
||||
#${OVERLAY_ID} .wesp-kiosk-menu-link:hover {
|
||||
background: #1b66a8;
|
||||
border-color: #4ea8ff;
|
||||
}
|
||||
#${OVERLAY_ID} .wesp-kiosk-menu-close {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
border: 1px solid #7a7a7a;
|
||||
border-radius: 10px;
|
||||
background: #3a3a3a;
|
||||
color: #fff;
|
||||
padding: 10px 12px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#${OVERLAY_ID} .wesp-kiosk-menu-close:hover {
|
||||
background: #505050;
|
||||
}
|
||||
.wesp-kiosk-menu-button {
|
||||
position: fixed !important;
|
||||
top: 16px !important;
|
||||
right: 16px !important;
|
||||
z-index: 4250 !important;
|
||||
width: 56px !important;
|
||||
height: 56px !important;
|
||||
border-radius: 50% !important;
|
||||
border: 1px solid #1f6fd6 !important;
|
||||
background: #1f6fd6 !important;
|
||||
color: #fff !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
padding: 0 !important;
|
||||
cursor: pointer !important;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35) !important;
|
||||
font-size: 30px !important;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
.wesp-kiosk-menu-button:hover {
|
||||
background: #1859a8 !important;
|
||||
transform: scale(1.04);
|
||||
}
|
||||
.wesp-kiosk-menu-button img {
|
||||
display: none !important;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.wesp-kiosk-menu-button {
|
||||
top: 12px !important;
|
||||
right: 12px !important;
|
||||
width: 50px !important;
|
||||
height: 50px !important;
|
||||
font-size: 28px !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function ensureOverlay() {
|
||||
ensureStyles();
|
||||
let overlay = document.getElementById(OVERLAY_ID);
|
||||
if (overlay) return overlay;
|
||||
|
||||
overlay = document.createElement("div");
|
||||
overlay.id = OVERLAY_ID;
|
||||
overlay.innerHTML = `
|
||||
<div class="wesp-kiosk-menu-panel" role="dialog" aria-modal="true" aria-label="Меню киоска">
|
||||
<div class="wesp-kiosk-menu-logo-wrap">
|
||||
<img src="/static/logo2.png" alt="WESP" class="wesp-kiosk-menu-logo">
|
||||
</div>
|
||||
<div class="wesp-kiosk-menu-links">
|
||||
<a class="wesp-kiosk-menu-link" href="/scales">Главная</a>
|
||||
<a class="wesp-kiosk-menu-link" href="#" id="calibrationLink">Калибровка</a>
|
||||
<a class="wesp-kiosk-menu-link" href="#" id="dbCheckLink">Проверка БД</a>
|
||||
<a class="wesp-kiosk-menu-link" href="#" id="pairTerminalLink">Привязать терминал</a>
|
||||
</div>
|
||||
<button type="button" class="wesp-kiosk-menu-close" id="wespKioskMenuClose">Закрыть</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const panel = overlay.querySelector(".wesp-kiosk-menu-panel");
|
||||
const closeBtn = overlay.querySelector("#wespKioskMenuClose");
|
||||
const close = () => {
|
||||
overlay.style.display = "none";
|
||||
};
|
||||
closeBtn.addEventListener("click", close);
|
||||
overlay.addEventListener("click", (e) => {
|
||||
if (e.target === overlay) close();
|
||||
});
|
||||
panel.addEventListener("click", (e) => e.stopPropagation());
|
||||
return overlay;
|
||||
}
|
||||
|
||||
function setup(opts) {
|
||||
const options = opts || {};
|
||||
const selector =
|
||||
options.menuButtonSelector || "#kioskMenuButton, .kiosk-menu-btn, .nav-button";
|
||||
const menuButton = document.querySelector(selector);
|
||||
if (!menuButton) return;
|
||||
|
||||
ensureStyles();
|
||||
const overlay = ensureOverlay();
|
||||
if (menuButton.dataset.wespMenuBound === "1") return;
|
||||
menuButton.classList.add("wesp-kiosk-menu-button");
|
||||
menuButton.setAttribute("title", "Меню");
|
||||
menuButton.setAttribute("aria-label", "Открыть меню");
|
||||
menuButton.textContent = "☰";
|
||||
menuButton.dataset.wespMenuBound = "1";
|
||||
menuButton.removeAttribute("onclick");
|
||||
menuButton.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
overlay.style.display = "flex";
|
||||
});
|
||||
}
|
||||
|
||||
window.WespKioskMenu = { setup, ensureOverlay };
|
||||
})();
|
||||
@@ -0,0 +1,285 @@
|
||||
(function () {
|
||||
const LOG = "[WespKioskPairing]";
|
||||
const API = {
|
||||
status: "/api/kiosk/status",
|
||||
accessLink: "/api/kiosk/access-link",
|
||||
setupAvailable: "/api/kiosk/setup-available",
|
||||
};
|
||||
const LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
|
||||
|
||||
function isLocalhostContext() {
|
||||
return LOCALHOST_HOSTS.has((window.location.hostname || "").toLowerCase());
|
||||
}
|
||||
|
||||
function ensureModalStyles() {
|
||||
const criticalId = "wespKioskPairCriticalCss";
|
||||
if (!document.getElementById(criticalId)) {
|
||||
const style = document.createElement("style");
|
||||
style.id = criticalId;
|
||||
style.textContent =
|
||||
"#kioskPairModal{display:none;position:fixed;inset:0;z-index:5000;padding:8px;background:rgba(0,0,0,.55);overflow:hidden}" +
|
||||
"#kioskPairModal.kiosk-pair-open{display:flex;align-items:stretch;justify-content:center;height:100vh;height:100dvh}" +
|
||||
"body.kiosk-pair-modal-open{overflow:hidden!important}" +
|
||||
"#kioskPairModal .kiosk-pair-panel{display:flex;flex-direction:column;width:min(480px,100%);height:100%;max-height:100%;min-height:0;overflow:hidden;box-sizing:border-box}" +
|
||||
"#kioskPairModal .kiosk-pair-body{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch}" +
|
||||
"#kioskPairModal .kiosk-pair-actions{flex:0 0 auto;display:flex;flex-wrap:wrap;gap:6px}";
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
if (document.getElementById("wespKioskModalsCss")) return;
|
||||
if (window.WespKioskTheme && typeof window.WespKioskTheme.ensureModalStyles === "function") {
|
||||
window.WespKioskTheme.ensureModalStyles();
|
||||
return;
|
||||
}
|
||||
const link = document.createElement("link");
|
||||
link.id = "wespKioskModalsCss";
|
||||
link.rel = "stylesheet";
|
||||
link.href = "/static/css/kiosk-modals.css";
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
function createModal() {
|
||||
ensureModalStyles();
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "kioskPairModal";
|
||||
modal.innerHTML = `
|
||||
<div class="kiosk-pair-panel" role="dialog" aria-modal="true" aria-labelledby="kioskPairTitle">
|
||||
<div class="kiosk-pair-header">
|
||||
<h3 id="kioskPairTitle">Привязать терминал</h3>
|
||||
<p class="kiosk-pair-subtitle">Start URL для Fully Kiosk Browser</p>
|
||||
</div>
|
||||
<div class="kiosk-pair-body">
|
||||
<div class="kiosk-pair-steps">
|
||||
1) Скопируйте ссылку или отсканируйте QR.<br/>
|
||||
2) Fully Kiosk → Settings → Start URL → вставьте ссылку → Save.<br/>
|
||||
3) Перезапустите Fully Kiosk.
|
||||
</div>
|
||||
<div id="kioskPairLoading" class="kiosk-pair-loading">
|
||||
<span class="kiosk-pair-spinner"></span>
|
||||
<span>Загрузка ссылки...</span>
|
||||
</div>
|
||||
<div class="kiosk-pair-qr-wrap">
|
||||
<img id="kioskPairQr" class="kiosk-pair-qr" alt="QR Start URL" />
|
||||
</div>
|
||||
<p id="kioskPairUrl" class="kiosk-pair-url"></p>
|
||||
<p id="kioskPairStatus" class="kiosk-pair-status"></p>
|
||||
</div>
|
||||
<div class="kiosk-pair-actions">
|
||||
<button id="kioskPairCopy" type="button" class="kiosk-pair-btn kiosk-pair-btn--primary">Скопировать ссылку</button>
|
||||
<button id="kioskPairRefresh" type="button" class="kiosk-pair-btn kiosk-pair-btn--secondary">Обновить ссылку</button>
|
||||
<button id="kioskPairClose" type="button" class="kiosk-pair-btn kiosk-pair-btn--ghost">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
return modal;
|
||||
}
|
||||
|
||||
function createBlockOverlay() {
|
||||
const el = document.createElement("div");
|
||||
el.id = "kioskBlockedOverlay";
|
||||
el.style.cssText =
|
||||
"display:none;position:fixed;inset:0;background:rgba(10,10,10,.78);z-index:4500;color:#fff;align-items:center;justify-content:center;text-align:center;padding:20px;";
|
||||
el.innerHTML = `
|
||||
<div style="max-width:520px">
|
||||
<h2 style="margin:0 0 10px 0">Терминал не привязан</h2>
|
||||
<p style="margin:0;line-height:1.4">Обратитесь к администратору для настройки терминала.</p>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
async function fetchJson(url, options) {
|
||||
const response = await fetch(url, options);
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
console.warn(LOG, url, response.status, data.message || data);
|
||||
throw new Error(data.message || `HTTP ${response.status}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function setup(opts) {
|
||||
const options = opts || {};
|
||||
const modal = createModal();
|
||||
const block = createBlockOverlay();
|
||||
const qrImg = modal.querySelector("#kioskPairQr");
|
||||
const qrWrap = modal.querySelector(".kiosk-pair-qr-wrap");
|
||||
const loadingEl = modal.querySelector("#kioskPairLoading");
|
||||
const urlEl = modal.querySelector("#kioskPairUrl");
|
||||
const statusEl = modal.querySelector("#kioskPairStatus");
|
||||
const copyBtn = modal.querySelector("#kioskPairCopy");
|
||||
const refreshBtn = modal.querySelector("#kioskPairRefresh");
|
||||
const closeBtn = modal.querySelector("#kioskPairClose");
|
||||
let currentStartUrl = "";
|
||||
let canIssueAccessLink = isLocalhostContext();
|
||||
|
||||
function isEnforcePairedOnly(data) {
|
||||
if (!data || typeof data !== "object") return true;
|
||||
return data.enforce_paired_only !== false;
|
||||
}
|
||||
|
||||
async function refreshSetupAvailability() {
|
||||
if (isLocalhostContext()) {
|
||||
canIssueAccessLink = true;
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const data = await fetchJson(API.setupAvailable);
|
||||
canIssueAccessLink = !!data.can_issue_access_link;
|
||||
} catch (e) {
|
||||
console.warn(LOG, "setup-available error", e);
|
||||
canIssueAccessLink = false;
|
||||
}
|
||||
if (options.menuLinkSelector) {
|
||||
const menuLink = document.querySelector(options.menuLinkSelector);
|
||||
if (menuLink) {
|
||||
menuLink.style.display = canIssueAccessLink ? "" : "none";
|
||||
}
|
||||
}
|
||||
return canIssueAccessLink;
|
||||
}
|
||||
|
||||
async function checkStatus() {
|
||||
if (isLocalhostContext()) {
|
||||
block.style.display = "none";
|
||||
if (typeof options.onPaired === "function") options.onPaired({ paired: true, localhost: true });
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const data = await fetchJson(API.status);
|
||||
const enforce = isEnforcePairedOnly(data);
|
||||
const paired = !!data.paired || !enforce;
|
||||
console.debug(LOG, "/api/kiosk/status", {
|
||||
paired: !!data.paired,
|
||||
enforce_paired_only: enforce,
|
||||
device_id: data.device_id,
|
||||
status: data.status,
|
||||
});
|
||||
block.style.display = paired ? "none" : "flex";
|
||||
if (paired && typeof options.onPaired === "function") options.onPaired(data);
|
||||
if (!paired && typeof options.onUnpaired === "function") options.onUnpaired(data);
|
||||
return paired;
|
||||
} catch (e) {
|
||||
console.warn(LOG, "checkStatus error", e);
|
||||
block.style.display = "flex";
|
||||
if (typeof options.onUnpaired === "function") options.onUnpaired({});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAccessLink(refresh) {
|
||||
if (!canIssueAccessLink) {
|
||||
statusEl.textContent = "Выдача Start URL доступна только администратору или с localhost.";
|
||||
statusEl.style.color = "#ff9f9f";
|
||||
return;
|
||||
}
|
||||
loadingEl.style.display = "flex";
|
||||
qrWrap.classList.remove("kiosk-pair-qr-visible");
|
||||
qrImg.removeAttribute("src");
|
||||
statusEl.textContent = refresh ? "Обновление ссылки..." : "Загрузка ссылки...";
|
||||
try {
|
||||
const data = await fetchJson(API.accessLink, {
|
||||
method: refresh ? "POST" : "GET",
|
||||
headers: refresh ? { "Content-Type": "application/json" } : undefined,
|
||||
body: refresh ? JSON.stringify({ refresh: true }) : undefined,
|
||||
});
|
||||
currentStartUrl = data.start_url || data.pair_url || "";
|
||||
console.info(LOG, "access-link OK", { start_url: currentStartUrl.slice(0, 80) });
|
||||
const qrSrc = data.qr_image_url || "";
|
||||
await new Promise((resolve) => {
|
||||
const done = () => {
|
||||
qrImg.onload = null;
|
||||
qrImg.onerror = null;
|
||||
resolve();
|
||||
};
|
||||
qrImg.onload = done;
|
||||
qrImg.onerror = done;
|
||||
qrImg.src = qrSrc;
|
||||
});
|
||||
loadingEl.style.display = "none";
|
||||
qrWrap.classList.add("kiosk-pair-qr-visible");
|
||||
qrImg.style.display = "block";
|
||||
urlEl.textContent = currentStartUrl;
|
||||
statusEl.textContent = "Постоянная ссылка. Вставьте её в Fully Kiosk → Start URL.";
|
||||
statusEl.style.color = "";
|
||||
} catch (e) {
|
||||
loadingEl.style.display = "none";
|
||||
statusEl.textContent = globalThis.WespUserMessages?.userFacingMessage?.(e.message, "Не удалось получить ссылку") || "Не удалось получить ссылку";
|
||||
statusEl.style.color = "#ff9f9f";
|
||||
}
|
||||
}
|
||||
|
||||
async function openModal() {
|
||||
if (!canIssueAccessLink) {
|
||||
window.alert("Выдача Start URL доступна только администратору или при открытии с localhost.");
|
||||
return;
|
||||
}
|
||||
modal.classList.add("kiosk-pair-open");
|
||||
document.body.classList.add("kiosk-pair-modal-open");
|
||||
loadAccessLink(false);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.classList.remove("kiosk-pair-open");
|
||||
document.body.classList.remove("kiosk-pair-modal-open");
|
||||
}
|
||||
|
||||
async function copyStartUrl() {
|
||||
if (!currentStartUrl) {
|
||||
statusEl.textContent = "Сначала дождитесь загрузки ссылки";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(currentStartUrl);
|
||||
} else {
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = currentStartUrl;
|
||||
ta.style.position = "fixed";
|
||||
ta.style.left = "-9999px";
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
statusEl.textContent = "Ссылка скопирована";
|
||||
statusEl.style.color = "#9fd89f";
|
||||
} catch (e) {
|
||||
statusEl.textContent = "Не удалось скопировать — выделите ссылку вручную";
|
||||
statusEl.style.color = "#ff9f9f";
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshLink() {
|
||||
if (!window.confirm("Старая ссылка перестанет работать. Обновить Start URL?")) {
|
||||
return;
|
||||
}
|
||||
await loadAccessLink(true);
|
||||
}
|
||||
|
||||
copyBtn.addEventListener("click", copyStartUrl);
|
||||
refreshBtn.addEventListener("click", refreshLink);
|
||||
closeBtn.addEventListener("click", closeModal);
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
|
||||
if (options.menuLinkSelector) {
|
||||
const menuLink = document.querySelector(options.menuLinkSelector);
|
||||
if (menuLink) {
|
||||
menuLink.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
openModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
refreshSetupAvailability().then(function () {
|
||||
checkStatus();
|
||||
});
|
||||
return { checkStatus, openModal };
|
||||
}
|
||||
|
||||
window.WespKioskPairing = { setup };
|
||||
})();
|
||||
@@ -0,0 +1,153 @@
|
||||
(function () {
|
||||
const KEY = "theme";
|
||||
const STYLE_ID = "wespKioskThemeStyles";
|
||||
const TOGGLE_ID = "wespThemeToggleWrap";
|
||||
|
||||
function savedTheme() {
|
||||
const t = (localStorage.getItem(KEY) || "").trim().toLowerCase();
|
||||
return t === "light" ? "light" : "dark";
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
const next = theme === "dark" ? "dark" : "light";
|
||||
document.documentElement.setAttribute("data-theme", next);
|
||||
localStorage.setItem(KEY, next);
|
||||
}
|
||||
|
||||
function injectModalChromeStyles() {
|
||||
const id = "wespKioskModalsCss";
|
||||
if (document.getElementById(id)) return;
|
||||
const link = document.createElement("link");
|
||||
link.id = id;
|
||||
link.rel = "stylesheet";
|
||||
link.href = "/static/css/kiosk-modals.css";
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
function ensureModalStyles() {
|
||||
injectModalChromeStyles();
|
||||
}
|
||||
|
||||
function injectStyles() {
|
||||
if (document.getElementById(STYLE_ID)) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = STYLE_ID;
|
||||
style.textContent = `
|
||||
[data-theme="dark"] body {
|
||||
background: #1a1a1a !important;
|
||||
color: #e6e6e6 !important;
|
||||
}
|
||||
[data-theme="dark"] .card,
|
||||
[data-theme="dark"] .data-container,
|
||||
[data-theme="dark"] .admin-panel,
|
||||
[data-theme="dark"] .modal-content,
|
||||
[data-theme="dark"] .modal,
|
||||
[data-theme="dark"] .stat-card,
|
||||
[data-theme="dark"] .table-section,
|
||||
[data-theme="dark"] .loading-content,
|
||||
[data-theme="dark"] .error-modal {
|
||||
background: #242424 !important;
|
||||
color: #f0f0f0 !important;
|
||||
border-color: #3a3a3a !important;
|
||||
}
|
||||
[data-theme="dark"] .subtitle,
|
||||
[data-theme="dark"] .meta,
|
||||
[data-theme="dark"] .stat-label,
|
||||
[data-theme="dark"] .text-muted {
|
||||
color: #bdbdbd !important;
|
||||
}
|
||||
[data-theme="dark"] .btn,
|
||||
[data-theme="dark"] .control-button,
|
||||
[data-theme="dark"] .numpad-btn {
|
||||
color: #fff !important;
|
||||
}
|
||||
[data-theme="dark"] pre {
|
||||
background: #181818 !important;
|
||||
color: #dcdcdc !important;
|
||||
border: 1px solid #353535 !important;
|
||||
}
|
||||
#${TOGGLE_ID} {
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
right: 16px;
|
||||
z-index: 4249;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
#${TOGGLE_ID} input {
|
||||
--s: 40px;
|
||||
height: var(--s);
|
||||
aspect-ratio: 2.5;
|
||||
width: auto;
|
||||
border-radius: var(--s);
|
||||
padding: calc(var(--s) / 10);
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
background:
|
||||
radial-gradient(farthest-side, #15202a 96%, #0000)
|
||||
var(--_p, 0%) / var(--s) content-box no-repeat,
|
||||
var(--_c, #ff7a7a);
|
||||
box-sizing: content-box;
|
||||
transform-origin: calc(3 * var(--s) / 5) 50%;
|
||||
transition:
|
||||
transform cubic-bezier(0, 300, 1, 300) .5s,
|
||||
background .3s .1s ease-in;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
#${TOGGLE_ID} input:checked {
|
||||
--_c: #85ff7a;
|
||||
--_p: 100%;
|
||||
transform-origin: calc(100% - 3 * var(--s) / 5) 50%;
|
||||
transform: rotate(0.1deg);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
#${TOGGLE_ID} {
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
transform: scale(0.9);
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function ensureToggle() {
|
||||
let wrap = document.getElementById(TOGGLE_ID);
|
||||
if (wrap) return wrap;
|
||||
wrap = document.createElement("button");
|
||||
wrap.id = TOGGLE_ID;
|
||||
wrap.type = "button";
|
||||
wrap.innerHTML = `<input id="wespThemeSwitch" type="checkbox" aria-label="Тёмная тема" title="Выключено — светлая тема. Включено — тёмная.">`;
|
||||
const input = wrap.querySelector("#wespThemeSwitch");
|
||||
input.addEventListener("change", function () {
|
||||
const next = input.checked ? "dark" : "light";
|
||||
applyTheme(next);
|
||||
});
|
||||
document.body.appendChild(wrap);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function setup(opts) {
|
||||
const options = opts || {};
|
||||
const theme = savedTheme();
|
||||
injectModalChromeStyles();
|
||||
injectStyles();
|
||||
applyTheme(theme);
|
||||
if (options.withToggle !== false) {
|
||||
const t = ensureToggle();
|
||||
const input = t.querySelector("#wespThemeSwitch");
|
||||
if (input) input.checked = theme === "dark";
|
||||
}
|
||||
}
|
||||
|
||||
window.WespKioskTheme = { setup, applyTheme, ensureModalStyles };
|
||||
})();
|
||||
@@ -72,11 +72,36 @@
|
||||
|
||||
function load(root, enterpriseId) {
|
||||
root.innerHTML = '<p class="text-muted small">Загрузка конфликтов…</p>';
|
||||
const orch = global.__WESP_ORCH__ || {};
|
||||
if (orch.configured === false) {
|
||||
root.innerHTML =
|
||||
'<p class="text-muted small">Orchestrator не настроен. Укажите upstream, hub_site_id и api_key в админке.</p>';
|
||||
return;
|
||||
}
|
||||
if (orch.reachable === false) {
|
||||
const host = orch.upstream ? ` (${orch.upstream})` : "";
|
||||
root.innerHTML =
|
||||
`<p class="text-warning small">Orchestrator недоступен${escapeHtml(host)}. Проверьте upstream URL и что сервис запущен.</p>`;
|
||||
return;
|
||||
}
|
||||
fetch(`${API}?enterprise_id=${encodeURIComponent(enterpriseId)}`)
|
||||
.then((r) => r.json())
|
||||
.then((items) => renderList(root, items || [], enterpriseId))
|
||||
.catch(() => {
|
||||
root.innerHTML = '<p class="text-danger small">Не удалось загрузить конфликты.</p>';
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
return r.json().catch(() => ({})).then((body) => {
|
||||
throw new Error(body.message || `HTTP ${r.status}`);
|
||||
});
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then((items) => {
|
||||
if (items && items.error) {
|
||||
throw new Error(items.message || "Orchestrator error");
|
||||
}
|
||||
renderList(root, Array.isArray(items) ? items : [], enterpriseId);
|
||||
})
|
||||
.catch((err) => {
|
||||
const msg = err && err.message ? err.message : "Не удалось загрузить конфликты.";
|
||||
root.innerHTML = `<p class="text-danger small">${escapeHtml(msg)}</p>`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -151,44 +151,11 @@ export function createRecipesAuthSettingsController({ notyf }) {
|
||||
}
|
||||
|
||||
function setupMobileMenu() {
|
||||
const hamburgerMenu = document.getElementById("hamburgerMenu");
|
||||
const mobileMenuOverlay = document.getElementById("mobileMenuOverlay");
|
||||
const closeMenu = document.getElementById("closeMenu");
|
||||
const mobileUserInfo = document.getElementById("mobileUserInfo");
|
||||
if (!hamburgerMenu || !mobileMenuOverlay || !closeMenu) return;
|
||||
|
||||
hamburgerMenu.addEventListener("click", () => {
|
||||
mobileMenuOverlay.classList.add("active");
|
||||
hamburgerMenu.classList.add("active");
|
||||
document.body.style.overflow = "hidden";
|
||||
const userInfo = document.getElementById("userInfo");
|
||||
if (userInfo && mobileUserInfo) {
|
||||
mobileUserInfo.textContent = userInfo.textContent || "";
|
||||
mobileUserInfo.textContent = userInfo.textContent?.trim() || "";
|
||||
}
|
||||
});
|
||||
|
||||
const closeMobileMenu = () => {
|
||||
mobileMenuOverlay.classList.remove("active");
|
||||
hamburgerMenu.classList.remove("active");
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
|
||||
closeMenu.addEventListener("click", closeMobileMenu);
|
||||
mobileMenuOverlay.addEventListener("click", (e) => {
|
||||
if (e.target === mobileMenuOverlay) closeMobileMenu();
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && mobileMenuOverlay.classList.contains("active")) {
|
||||
closeMobileMenu();
|
||||
}
|
||||
});
|
||||
|
||||
const mobileMenuItems = document.querySelectorAll(".mobile-menu-item");
|
||||
mobileMenuItems.forEach((item) => {
|
||||
item.addEventListener("click", () => {
|
||||
setTimeout(closeMobileMenu, 300);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSyncClientsSettings() {
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
let activePreset = "day";
|
||||
let panelEl = null;
|
||||
let triggerEl = null;
|
||||
let isOpen = false;
|
||||
let suppressOutsideCloseUntil = 0;
|
||||
|
||||
function pad(n) {
|
||||
return String(n).padStart(2, "0");
|
||||
@@ -90,7 +92,7 @@
|
||||
|
||||
function setPresetActive(preset) {
|
||||
activePreset = preset || "";
|
||||
document.querySelectorAll("[data-drp-preset]").forEach((btn) => {
|
||||
panelEl?.querySelectorAll("[data-drp-preset]").forEach((btn) => {
|
||||
btn.classList.toggle("active", btn.getAttribute("data-drp-preset") === activePreset);
|
||||
});
|
||||
}
|
||||
@@ -131,7 +133,7 @@
|
||||
}
|
||||
|
||||
function positionPanel() {
|
||||
if (!triggerEl || !panelEl || panelEl.hidden) return;
|
||||
if (!triggerEl || !panelEl || !isOpen) return;
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
const width = Math.max(rect.width, 304);
|
||||
let left = rect.left;
|
||||
@@ -149,28 +151,51 @@
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
if (panelEl) panelEl.hidden = true;
|
||||
isOpen = false;
|
||||
suppressOutsideCloseUntil = 0;
|
||||
if (panelEl) {
|
||||
panelEl.hidden = true;
|
||||
panelEl.setAttribute("hidden", "");
|
||||
panelEl.classList.remove("reports-drp__panel--open");
|
||||
panelEl.style.left = "";
|
||||
panelEl.style.top = "";
|
||||
panelEl.style.width = "";
|
||||
}
|
||||
if (triggerEl) triggerEl.setAttribute("aria-expanded", "false");
|
||||
window.removeEventListener("resize", positionPanel);
|
||||
window.removeEventListener("scroll", positionPanel, true);
|
||||
}
|
||||
|
||||
function isPanelTarget(target) {
|
||||
if (!target || !panelEl || !triggerEl) return false;
|
||||
return triggerEl.contains(target) || panelEl.contains(target);
|
||||
}
|
||||
|
||||
function openPanel() {
|
||||
const committed = readCommittedRange();
|
||||
draftFrom = committed.from || today();
|
||||
draftTo = committed.to || draftFrom;
|
||||
viewMonth = new Date(draftFrom.getFullYear(), draftFrom.getMonth(), 1);
|
||||
renderCalendar();
|
||||
if (panelEl) panelEl.hidden = false;
|
||||
global.WespReportsFilterDropdown?.closeAll?.();
|
||||
global.WespReportsExport?.close?.();
|
||||
isOpen = true;
|
||||
suppressOutsideCloseUntil = Date.now() + 180;
|
||||
if (panelEl) {
|
||||
panelEl.removeAttribute("hidden");
|
||||
panelEl.hidden = false;
|
||||
panelEl.classList.add("reports-drp__panel--open");
|
||||
}
|
||||
if (triggerEl) triggerEl.setAttribute("aria-expanded", "true");
|
||||
positionPanel();
|
||||
requestAnimationFrame(() => positionPanel());
|
||||
window.addEventListener("resize", positionPanel);
|
||||
window.addEventListener("scroll", positionPanel, true);
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
if (panelEl?.hidden) openPanel();
|
||||
else closePanel();
|
||||
if (isOpen) closePanel();
|
||||
else openPanel();
|
||||
}
|
||||
|
||||
function inRange(d, a, b) {
|
||||
@@ -246,6 +271,8 @@
|
||||
triggerEl = document.getElementById("reportsDateRangeTrigger");
|
||||
panelEl = document.getElementById("reportsDateRangePanel");
|
||||
if (!triggerEl || !panelEl) return;
|
||||
if (triggerEl.dataset.drpWired === "1") return;
|
||||
triggerEl.dataset.drpWired = "1";
|
||||
|
||||
if (panelEl.parentElement !== document.body) {
|
||||
document.body.appendChild(panelEl);
|
||||
@@ -254,7 +281,7 @@
|
||||
panelEl.addEventListener("mousedown", (e) => e.stopPropagation());
|
||||
panelEl.addEventListener("click", (e) => e.stopPropagation());
|
||||
|
||||
document.querySelectorAll("[data-drp-preset]").forEach((btn) => {
|
||||
panelEl.querySelectorAll("[data-drp-preset]").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
setPeriod(btn.getAttribute("data-drp-preset") || "day");
|
||||
@@ -276,22 +303,36 @@
|
||||
positionPanel();
|
||||
});
|
||||
|
||||
triggerEl.addEventListener("click", (e) => {
|
||||
triggerEl.addEventListener(
|
||||
"pointerdown",
|
||||
(e) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
togglePanel();
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
document.addEventListener("click", () => {
|
||||
if (!panelEl.hidden) closePanel();
|
||||
});
|
||||
document.addEventListener(
|
||||
"pointerdown",
|
||||
(e) => {
|
||||
if (Date.now() < suppressOutsideCloseUntil) return;
|
||||
if (!isOpen) return;
|
||||
if (isPanelTarget(e.target)) return;
|
||||
closePanel();
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") closePanel();
|
||||
if (e.key === "Escape" && isOpen) closePanel();
|
||||
});
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const highlightReportId = urlParams.get("report");
|
||||
if highlightReportId) {
|
||||
if (highlightReportId) {
|
||||
global.__wespHighlightReportId = highlightReportId;
|
||||
setPeriod("month");
|
||||
} else {
|
||||
@@ -305,6 +346,7 @@
|
||||
init,
|
||||
setPeriod,
|
||||
commit: commitDraft,
|
||||
close: closePanel,
|
||||
updateTriggerLabel,
|
||||
formatLabel,
|
||||
parseIso,
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
* Экспорт /reports — выбор раздела и формата (Excel / PDF).
|
||||
*/
|
||||
(function (global) {
|
||||
let btnEl = null;
|
||||
let menuEl = null;
|
||||
let isOpen = false;
|
||||
let suppressOutsideCloseUntil = 0;
|
||||
|
||||
function appendFilterContext(params) {
|
||||
const farms = global.WespReportsFilters?.getSelectedFarmNames?.() || [];
|
||||
const dispensers = global.WespReportsFilters?.getSelectedDispenserNames?.() || [];
|
||||
@@ -34,34 +39,89 @@
|
||||
window.location.href = buildExportUrl(format, section);
|
||||
}
|
||||
|
||||
function isMenuTarget(target) {
|
||||
if (!target || !btnEl || !menuEl) return false;
|
||||
return btnEl.contains(target) || menuEl.contains(target);
|
||||
}
|
||||
|
||||
function positionMenu() {
|
||||
if (!btnEl || !menuEl || !isOpen) return;
|
||||
const rect = btnEl.getBoundingClientRect();
|
||||
let top = rect.bottom + 6;
|
||||
const menuHeight = menuEl.offsetHeight || 320;
|
||||
if (top + menuHeight > window.innerHeight - 12) {
|
||||
top = Math.max(12, rect.top - menuHeight - 6);
|
||||
}
|
||||
menuEl.style.top = `${top}px`;
|
||||
menuEl.style.right = `${Math.max(12, window.innerWidth - rect.right)}px`;
|
||||
menuEl.style.left = "auto";
|
||||
menuEl.style.width = "";
|
||||
menuEl.style.transform = "";
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
const menu = document.getElementById("reportsExportMenu");
|
||||
const btn = document.getElementById("reportsExportBtn");
|
||||
if (menu) menu.hidden = true;
|
||||
if (btn) btn.setAttribute("aria-expanded", "false");
|
||||
isOpen = false;
|
||||
suppressOutsideCloseUntil = 0;
|
||||
if (menuEl) {
|
||||
menuEl.hidden = true;
|
||||
menuEl.setAttribute("hidden", "");
|
||||
menuEl.classList.remove("reports-export-dropdown__menu--open");
|
||||
menuEl.style.top = "";
|
||||
menuEl.style.right = "";
|
||||
menuEl.style.left = "";
|
||||
menuEl.style.width = "";
|
||||
menuEl.style.transform = "";
|
||||
}
|
||||
if (btnEl) btnEl.setAttribute("aria-expanded", "false");
|
||||
window.removeEventListener("resize", positionMenu);
|
||||
window.removeEventListener("scroll", positionMenu, true);
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
global.WespReportsDateRange?.close?.();
|
||||
global.WespReportsFilterDropdown?.closeAll?.();
|
||||
isOpen = true;
|
||||
suppressOutsideCloseUntil = Date.now() + 180;
|
||||
if (menuEl) {
|
||||
menuEl.removeAttribute("hidden");
|
||||
menuEl.hidden = false;
|
||||
menuEl.classList.add("reports-export-dropdown__menu--open");
|
||||
}
|
||||
if (btnEl) btnEl.setAttribute("aria-expanded", "true");
|
||||
positionMenu();
|
||||
requestAnimationFrame(() => positionMenu());
|
||||
window.addEventListener("resize", positionMenu);
|
||||
window.addEventListener("scroll", positionMenu, true);
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
const menu = document.getElementById("reportsExportMenu");
|
||||
const btn = document.getElementById("reportsExportBtn");
|
||||
if (!menu || !btn) return;
|
||||
const open = menu.hidden;
|
||||
menu.hidden = !open;
|
||||
btn.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
if (isOpen) closeMenu();
|
||||
else openMenu();
|
||||
}
|
||||
|
||||
function init() {
|
||||
const btn = document.getElementById("reportsExportBtn");
|
||||
const menu = document.getElementById("reportsExportMenu");
|
||||
if (!btn || !menu || btn.dataset.bound) return;
|
||||
btn.dataset.bound = "1";
|
||||
btnEl = document.getElementById("reportsExportBtn");
|
||||
menuEl = document.getElementById("reportsExportMenu");
|
||||
if (!btnEl || !menuEl || btnEl.dataset.bound) return;
|
||||
btnEl.dataset.bound = "1";
|
||||
|
||||
btn.addEventListener("click", (e) => {
|
||||
if (menuEl.parentElement !== document.body) {
|
||||
document.body.appendChild(menuEl);
|
||||
}
|
||||
|
||||
btnEl.addEventListener(
|
||||
"pointerdown",
|
||||
(e) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
toggleMenu();
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
menu.querySelectorAll("[data-reports-export-format]").forEach((item) => {
|
||||
menuEl.querySelectorAll("[data-reports-export-format]").forEach((item) => {
|
||||
item.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -73,14 +133,30 @@
|
||||
});
|
||||
});
|
||||
|
||||
menu.addEventListener("mousedown", (e) => e.stopPropagation());
|
||||
menu.addEventListener("click", (e) => e.stopPropagation());
|
||||
menuEl.addEventListener("mousedown", (e) => e.stopPropagation());
|
||||
menuEl.addEventListener("click", (e) => e.stopPropagation());
|
||||
|
||||
document.addEventListener(
|
||||
"pointerdown",
|
||||
(e) => {
|
||||
if (Date.now() < suppressOutsideCloseUntil) return;
|
||||
if (!isOpen) return;
|
||||
if (isMenuTarget(e.target)) return;
|
||||
closeMenu();
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
document.addEventListener("click", () => closeMenu());
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") closeMenu();
|
||||
if (e.key === "Escape" && isOpen) closeMenu();
|
||||
});
|
||||
}
|
||||
|
||||
global.WespReportsExport = { init, buildExportUrl, download, appendFilterContext };
|
||||
global.WespReportsExport = {
|
||||
init,
|
||||
buildExportUrl,
|
||||
download,
|
||||
appendFilterContext,
|
||||
close: closeMenu,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
@@ -217,6 +217,7 @@
|
||||
trigger.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const willOpen = menu.hidden;
|
||||
global.WespReportsDateRange?.close?.();
|
||||
closeAll(willOpen ? menu : null);
|
||||
menu.hidden = !willOpen;
|
||||
trigger.setAttribute("aria-expanded", willOpen ? "true" : "false");
|
||||
@@ -250,11 +251,25 @@
|
||||
return api;
|
||||
}
|
||||
|
||||
function isReportsPopoverTarget(target) {
|
||||
if (!target) return false;
|
||||
const roots = [
|
||||
"reportsDateRangeTrigger",
|
||||
"reportsDateRangePanel",
|
||||
"reportsExportBtn",
|
||||
"reportsExportMenu",
|
||||
];
|
||||
return roots.some((id) => document.getElementById(id)?.contains(target));
|
||||
}
|
||||
|
||||
function init() {
|
||||
bindSelect("farm-select");
|
||||
bindSelect("dispenser-select");
|
||||
bindSelect("recipe-select");
|
||||
document.addEventListener("click", () => closeAll());
|
||||
document.addEventListener("click", (e) => {
|
||||
if (isReportsPopoverTarget(e.target)) return;
|
||||
closeAll();
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") closeAll();
|
||||
});
|
||||
@@ -278,5 +293,6 @@
|
||||
bindSelect,
|
||||
getMultiSelectValues,
|
||||
setMultiSelectValues,
|
||||
closeAll,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
@@ -132,6 +132,15 @@
|
||||
|
||||
function buildAlertsParams() {
|
||||
const params = new URLSearchParams(getDateParams());
|
||||
const recipeIds = getRecipeIds();
|
||||
if (recipeIds.length === 1) {
|
||||
params.set("recipe_id", recipeIds[0]);
|
||||
return params;
|
||||
}
|
||||
if (recipeIds.length > 1) {
|
||||
params.set("recipe_ids", recipeIds.join(","));
|
||||
return params;
|
||||
}
|
||||
const dispenserIds = getDispenserIds();
|
||||
if (dispenserIds.length === 1) {
|
||||
params.set("dispenser_id", dispenserIds[0]);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/** Progress 0–100 for logo loader during setup wizard. */
|
||||
|
||||
const STEP_WEIGHTS = {
|
||||
theme: 10,
|
||||
welcome: 5,
|
||||
device: 12,
|
||||
network: 18,
|
||||
sync: 18,
|
||||
users: 22,
|
||||
hardware: 10,
|
||||
kiosk: 10,
|
||||
bootstrap: 25,
|
||||
done: 8,
|
||||
};
|
||||
|
||||
const FLOWS = {
|
||||
server: ["theme", "welcome", "device", "network", "users", "done"],
|
||||
client: ["theme", "welcome", "device", "sync", "hardware", "kiosk", "bootstrap", "done"],
|
||||
};
|
||||
|
||||
export function getFlow(deviceRole) {
|
||||
return FLOWS[deviceRole] || FLOWS.server;
|
||||
}
|
||||
|
||||
export function createSetupOrchestrator() {
|
||||
let deviceRole = "server";
|
||||
let currentStep = "welcome";
|
||||
let extraPercent = 0;
|
||||
|
||||
function flow() {
|
||||
return getFlow(deviceRole);
|
||||
}
|
||||
|
||||
function basePercent() {
|
||||
const steps = flow();
|
||||
const idx = steps.indexOf(currentStep);
|
||||
if (idx < 0) return 0;
|
||||
let total = 0;
|
||||
for (const s of steps) total += STEP_WEIGHTS[s] || 10;
|
||||
let done = 0;
|
||||
for (let i = 0; i < idx; i += 1) done += STEP_WEIGHTS[steps[i]] || 10;
|
||||
const curWeight = STEP_WEIGHTS[currentStep] || 10;
|
||||
return Math.min(100, Math.round(((done + curWeight * 0.35) / total) * 100));
|
||||
}
|
||||
|
||||
return {
|
||||
get percent() {
|
||||
return Math.min(100, Math.max(basePercent(), extraPercent));
|
||||
},
|
||||
get currentStep() {
|
||||
return currentStep;
|
||||
},
|
||||
get deviceRole() {
|
||||
return deviceRole;
|
||||
},
|
||||
setDeviceRole(role) {
|
||||
deviceRole = role;
|
||||
},
|
||||
setStep(step) {
|
||||
currentStep = step;
|
||||
extraPercent = 0;
|
||||
},
|
||||
bumpExtra(delta) {
|
||||
extraPercent = Math.min(100, extraPercent + delta);
|
||||
},
|
||||
setBootstrapProgress(p) {
|
||||
const steps = flow();
|
||||
const idx = steps.indexOf("bootstrap");
|
||||
if (idx < 0) return;
|
||||
let total = 0;
|
||||
let before = 0;
|
||||
for (let i = 0; i < steps.length; i += 1) {
|
||||
const w = STEP_WEIGHTS[steps[i]] || 10;
|
||||
if (i < idx) before += w;
|
||||
total += w;
|
||||
}
|
||||
const w = STEP_WEIGHTS.bootstrap || 25;
|
||||
extraPercent = Math.round(((before + w * Math.min(1, p)) / total) * 100);
|
||||
},
|
||||
flow,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* On-screen keyboard for setup wizard on touch kiosks (no system IME).
|
||||
* Uses wesp-setup CSS variables for light/dark theme.
|
||||
*/
|
||||
|
||||
const LAYOUTS = {
|
||||
hostname: [
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
|
||||
["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
|
||||
[
|
||||
"a",
|
||||
"s",
|
||||
"d",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
{ key: "backspace", label: "⌫", span: 2 },
|
||||
],
|
||||
["z", "x", "c", "v", "b", "n", "m", "_", "-", "."],
|
||||
],
|
||||
url: [
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
|
||||
["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
|
||||
[
|
||||
"a",
|
||||
"s",
|
||||
"d",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
{ key: "backspace", label: "⌫", span: 2 },
|
||||
],
|
||||
["z", "x", "c", "v", "b", "n", "m", "_", "-", "."],
|
||||
[
|
||||
{ key: "http://", label: "http://", span: 3 },
|
||||
{ key: ":", label: ":" },
|
||||
{ key: "/", label: "/" },
|
||||
{ key: "backspace", label: "⌫", span: 2 },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const HOSTNAME_CHAR = /[a-z0-9._-]/;
|
||||
const URL_CHAR = /[a-z0-9.:/_-]/;
|
||||
|
||||
function allowedChar(ch, layout) {
|
||||
if (layout === "url") return URL_CHAR.test(ch);
|
||||
return HOSTNAME_CHAR.test(ch);
|
||||
}
|
||||
|
||||
function normalizeInsert(ch, layout) {
|
||||
const lower = ch.toLowerCase();
|
||||
if (ch.length === 1 && !allowedChar(lower, layout)) return "";
|
||||
return lower;
|
||||
}
|
||||
|
||||
export class SetupTouchKeyboard {
|
||||
#panel;
|
||||
#root = null;
|
||||
#grid = null;
|
||||
#activeInput = null;
|
||||
#activeLayout = "hostname";
|
||||
#bindings = new Map();
|
||||
#docClickHandler = null;
|
||||
#maxLength = 200;
|
||||
|
||||
constructor(panel) {
|
||||
this.#panel = panel;
|
||||
}
|
||||
|
||||
attach(input, { layout = "hostname", maxLength = 200 } = {}) {
|
||||
if (!input || input.disabled) return;
|
||||
this.#bindings.set(input, { layout, maxLength });
|
||||
input.readOnly = true;
|
||||
input.inputMode = "none";
|
||||
input.autocomplete = "off";
|
||||
input.autocorrect = "off";
|
||||
input.spellcheck = false;
|
||||
input.classList.add("wesp-setup__input--touch");
|
||||
|
||||
const onFocus = () => this.#showFor(input);
|
||||
input.addEventListener("focus", onFocus);
|
||||
input.addEventListener("click", onFocus);
|
||||
}
|
||||
|
||||
mount() {
|
||||
if (this.#root) return;
|
||||
this.#root = document.createElement("div");
|
||||
this.#root.className = "wesp-setup__keyboard";
|
||||
this.#root.setAttribute("role", "group");
|
||||
this.#root.setAttribute("aria-label", "Экранная клавиатура");
|
||||
|
||||
this.#grid = document.createElement("div");
|
||||
this.#grid.className = "wesp-setup__keyboard-grid";
|
||||
this.#root.appendChild(this.#grid);
|
||||
|
||||
const footer = this.#panel.querySelector(".wesp-setup__footer");
|
||||
if (footer) {
|
||||
this.#panel.insertBefore(this.#root, footer);
|
||||
} else {
|
||||
this.#panel.appendChild(this.#root);
|
||||
}
|
||||
|
||||
this.#docClickHandler = (e) => {
|
||||
if (!this.#root?.classList.contains("is-visible")) return;
|
||||
const t = e.target;
|
||||
if (this.#root.contains(t)) return;
|
||||
if (this.#activeInput && (t === this.#activeInput || this.#activeInput.contains?.(t))) return;
|
||||
for (const input of this.#bindings.keys()) {
|
||||
if (t === input || input.contains?.(t)) return;
|
||||
}
|
||||
this.hide();
|
||||
};
|
||||
document.addEventListener("pointerdown", this.#docClickHandler, true);
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.#root?.classList.remove("is-visible");
|
||||
this.#activeInput = null;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.#docClickHandler) {
|
||||
document.removeEventListener("pointerdown", this.#docClickHandler, true);
|
||||
this.#docClickHandler = null;
|
||||
}
|
||||
this.#root?.remove();
|
||||
this.#root = null;
|
||||
this.#grid = null;
|
||||
this.#bindings.clear();
|
||||
this.#activeInput = null;
|
||||
}
|
||||
|
||||
#showFor(input) {
|
||||
const binding = this.#bindings.get(input);
|
||||
if (!binding) return;
|
||||
this.mount();
|
||||
this.#activeInput = input;
|
||||
this.#activeLayout = binding.layout;
|
||||
this.#maxLength = binding.maxLength;
|
||||
this.#renderKeys(binding.layout);
|
||||
this.#root.classList.add("is-visible");
|
||||
window.requestAnimationFrame(() => {
|
||||
input.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||
});
|
||||
}
|
||||
|
||||
#renderKeys(layoutName) {
|
||||
const rows = LAYOUTS[layoutName] || LAYOUTS.hostname;
|
||||
this.#grid.replaceChildren();
|
||||
rows.forEach((row) => {
|
||||
row.forEach((item) => {
|
||||
const spec = typeof item === "string" ? { key: item, label: item } : item;
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "wesp-setup__keyboard-key";
|
||||
if (spec.key === "backspace") btn.classList.add("wesp-setup__keyboard-key--wide");
|
||||
if (spec.key === "http://") btn.classList.add("wesp-setup__keyboard-key--action");
|
||||
btn.textContent = spec.label || spec.key;
|
||||
const span = spec.span || 1;
|
||||
if (span > 1) btn.style.gridColumn = `span ${span}`;
|
||||
btn.addEventListener("pointerdown", (e) => {
|
||||
e.preventDefault();
|
||||
this.#onKey(spec.key);
|
||||
});
|
||||
this.#grid.appendChild(btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#onKey(key) {
|
||||
const input = this.#activeInput;
|
||||
if (!input) return;
|
||||
if (key === "backspace") {
|
||||
input.value = input.value.slice(0, -1);
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
return;
|
||||
}
|
||||
let insert = "";
|
||||
if (key === "http://") {
|
||||
if (this.#activeLayout === "url") insert = "http://";
|
||||
} else {
|
||||
insert = normalizeInsert(key, this.#activeLayout);
|
||||
}
|
||||
if (!insert) return;
|
||||
if (input.value.length >= this.#maxLength) return;
|
||||
const start = input.selectionStart ?? input.value.length;
|
||||
const end = input.selectionEnd ?? start;
|
||||
input.value = input.value.slice(0, start) + insert + input.value.slice(end);
|
||||
const pos = start + insert.length;
|
||||
try {
|
||||
input.setSelectionRange(pos, pos);
|
||||
} catch {
|
||||
/* readonly selection may fail in some browsers */
|
||||
}
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* Диалоги alert/confirm для веб-оболочки зоотехника (не скрипты страниц киоска).
|
||||
* API: WespDialog.alert(msg, opts?) -> Promise<void>
|
||||
* WespDialog.confirm(msg, opts?) -> Promise<boolean>
|
||||
* opts.timerOnly — закрытие только через WespDialog.closeActive() (таймер); без кнопки, без Escape и клика по фону.
|
||||
*/
|
||||
(function (global) {
|
||||
var OVERLAY_ID = "wespShellDialogOverlay";
|
||||
var STYLE_ID = "wesp-shell-dialog-styles";
|
||||
|
||||
function injectStyles() {
|
||||
if (document.getElementById(STYLE_ID)) return;
|
||||
var s = document.createElement("style");
|
||||
s.id = STYLE_ID;
|
||||
s.textContent =
|
||||
"#" +
|
||||
OVERLAY_ID +
|
||||
"{" +
|
||||
"position:fixed;inset:0;z-index:5000;" +
|
||||
"display:none;align-items:center;justify-content:center;" +
|
||||
"padding:16px;background:rgba(0,0,0,.55);" +
|
||||
"box-sizing:border-box;" +
|
||||
"opacity:0;transition:opacity .22s cubic-bezier(.22,1,.36,1);" +
|
||||
"backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);" +
|
||||
"}" +
|
||||
"#" +
|
||||
OVERLAY_ID +
|
||||
'[aria-hidden="false"]{display:flex;opacity:1;}' +
|
||||
"#" +
|
||||
OVERLAY_ID +
|
||||
".wesp-shell-dialog-overlay-closing{opacity:0;}" +
|
||||
".wesp-shell-dialog{" +
|
||||
"width:min(420px,100%);max-height:min(80vh,520px);overflow:auto;" +
|
||||
"background:#fff;color:#111827;border-radius:8px;" +
|
||||
"border:1px solid #e5e7eb;box-shadow:0 8px 32px rgba(0,0,0,.18);" +
|
||||
"padding:20px 22px;font-family:var(--zootech-font,Inter,system-ui,sans-serif);" +
|
||||
"-webkit-font-smoothing:antialiased;" +
|
||||
"text-align:left;" +
|
||||
"opacity:0;transform:scale(.97) translateY(8px);" +
|
||||
"transition:opacity .22s cubic-bezier(.22,1,.36,1),transform .22s cubic-bezier(.22,1,.36,1);" +
|
||||
"}" +
|
||||
"#" +
|
||||
OVERLAY_ID +
|
||||
'[aria-hidden="false"]:not(.wesp-shell-dialog-overlay-closing) .wesp-shell-dialog{' +
|
||||
"opacity:1;transform:scale(1) translateY(0);" +
|
||||
"}" +
|
||||
"#" +
|
||||
OVERLAY_ID +
|
||||
".wesp-shell-dialog-overlay-closing .wesp-shell-dialog{" +
|
||||
"opacity:0;transform:scale(.97) translateY(8px);" +
|
||||
"}" +
|
||||
"@media (prefers-reduced-motion:reduce){" +
|
||||
"#" +
|
||||
OVERLAY_ID +
|
||||
",.wesp-shell-dialog{transition:none!important;transform:none!important;}" +
|
||||
"#" +
|
||||
OVERLAY_ID +
|
||||
"{backdrop-filter:none;-webkit-backdrop-filter:none;}" +
|
||||
".wesp-shell-dialog{opacity:1!important;}" +
|
||||
"}" +
|
||||
".wesp-shell-dialog-title{" +
|
||||
"margin:0 0 12px 0;font-size:1.125rem;font-weight:600;color:#111827;" +
|
||||
"}" +
|
||||
".wesp-shell-dialog-body{" +
|
||||
"margin:0;font-size:0.95rem;line-height:1.5;color:#374151;white-space:pre-wrap;word-break:break-word;" +
|
||||
"}" +
|
||||
".wesp-shell-dialog-actions{" +
|
||||
"display:flex;justify-content:flex-end;flex-wrap:wrap;gap:10px;margin-top:20px;" +
|
||||
"}" +
|
||||
".wesp-shell-dialog-btn{" +
|
||||
"min-width:120px;padding:10px 18px;border-radius:6px;font-size:0.95rem;font-weight:500;" +
|
||||
"cursor:pointer;font-family:inherit;border:1px solid #e5e7eb;background:#fff;color:#374151;" +
|
||||
"transition:background .2s,border-color .2s,box-shadow .2s;" +
|
||||
"}" +
|
||||
".wesp-shell-dialog-btn:hover{filter:brightness(.97);}" +
|
||||
".wesp-shell-dialog-btn-primary{" +
|
||||
"background:#2563eb;border-color:#2563eb;color:#fff;" +
|
||||
"}" +
|
||||
".wesp-shell-dialog-btn-primary:hover{box-shadow:0 4px 12px rgba(37,99,235,.25);}" +
|
||||
".wesp-shell-dialog-btn-danger{" +
|
||||
"background:#ef4444;border-color:#ef4444;color:#fff;" +
|
||||
"}" +
|
||||
".wesp-shell-dialog--danger{border-top:4px solid #ef4444;}" +
|
||||
".wesp-shell-dialog--success{border-top:4px solid #10b981;}" +
|
||||
"html[data-theme=dark] #" +
|
||||
OVERLAY_ID +
|
||||
" .wesp-shell-dialog{" +
|
||||
"background:var(--dm-surface,#2c3338);color:var(--dm-text,#f5f5f5);border-color:var(--dm-border,rgba(255,255,255,.12));" +
|
||||
"}" +
|
||||
"html[data-theme=dark] #" +
|
||||
OVERLAY_ID +
|
||||
" .wesp-shell-dialog-title{color:var(--dm-text,#f5f5f5);}" +
|
||||
"html[data-theme=dark] #" +
|
||||
OVERLAY_ID +
|
||||
" .wesp-shell-dialog-body{color:var(--dm-text,#f5f5f5);}" +
|
||||
"html[data-theme=dark] #" +
|
||||
OVERLAY_ID +
|
||||
" .wesp-shell-dialog-btn{" +
|
||||
"background:var(--dm-field,#3e444a);border-color:var(--dm-border,rgba(255,255,255,.12));color:var(--dm-text,#f5f5f5);" +
|
||||
"}" +
|
||||
"html[data-theme=dark] #" +
|
||||
OVERLAY_ID +
|
||||
" .wesp-shell-dialog-btn-primary{background:#2563eb;border-color:#2563eb;color:#fff;}" +
|
||||
"html[data-theme=dark] #" +
|
||||
OVERLAY_ID +
|
||||
" .wesp-shell-dialog-btn-danger{background:#dc2626;border-color:#dc2626;color:#fff;}" +
|
||||
"";
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function ensureDom() {
|
||||
injectStyles();
|
||||
var el = document.getElementById(OVERLAY_ID);
|
||||
if (el) return el;
|
||||
el = document.createElement("div");
|
||||
el.id = OVERLAY_ID;
|
||||
el.setAttribute("aria-hidden", "true");
|
||||
el.innerHTML =
|
||||
'<div class="wesp-shell-dialog" role="dialog" aria-modal="true" aria-labelledby="wespShellDialogTitle">' +
|
||||
'<h2 class="wesp-shell-dialog-title" id="wespShellDialogTitle"></h2>' +
|
||||
'<p class="wesp-shell-dialog-body" id="wespShellDialogBody"></p>' +
|
||||
'<div class="wesp-shell-dialog-actions" id="wespShellDialogActions"></div>' +
|
||||
"</div>";
|
||||
el.addEventListener("click", function (e) {
|
||||
if (e.target === el) {
|
||||
backdropDismiss();
|
||||
}
|
||||
});
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
var pendingResolve = null;
|
||||
var pendingMode = null;
|
||||
var blockUserDismiss = false;
|
||||
|
||||
function backdropDismiss() {
|
||||
if (blockUserDismiss) return;
|
||||
if (pendingMode === "confirm" && pendingResolve) {
|
||||
var fn = pendingResolve;
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
fn(false);
|
||||
} else if (pendingMode === "alert" && pendingResolve) {
|
||||
var fn2 = pendingResolve;
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
fn2();
|
||||
}
|
||||
}
|
||||
|
||||
function prefersReducedMotion() {
|
||||
return (
|
||||
global.matchMedia &&
|
||||
global.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
);
|
||||
}
|
||||
|
||||
function hide() {
|
||||
var el = document.getElementById(OVERLAY_ID);
|
||||
if (!el || el.getAttribute("aria-hidden") === "true") return;
|
||||
var dlg = el.querySelector(".wesp-shell-dialog");
|
||||
if (dlg) dlg.removeAttribute("tabindex");
|
||||
blockUserDismiss = false;
|
||||
document.removeEventListener("keydown", onKey);
|
||||
|
||||
if (prefersReducedMotion()) {
|
||||
el.classList.remove("wesp-shell-dialog-overlay-closing");
|
||||
el.setAttribute("aria-hidden", "true");
|
||||
return;
|
||||
}
|
||||
|
||||
el.classList.add("wesp-shell-dialog-overlay-closing");
|
||||
var done = false;
|
||||
var finish = function () {
|
||||
if (done) return;
|
||||
done = true;
|
||||
el.classList.remove("wesp-shell-dialog-overlay-closing");
|
||||
el.setAttribute("aria-hidden", "true");
|
||||
};
|
||||
el.addEventListener(
|
||||
"transitionend",
|
||||
function (e) {
|
||||
if (e.target === el) finish();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
global.setTimeout(finish, 280);
|
||||
}
|
||||
|
||||
function closeActive() {
|
||||
if (pendingMode === "confirm" && pendingResolve) {
|
||||
var fnC = pendingResolve;
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
fnC(false);
|
||||
} else if (pendingMode === "alert" && pendingResolve) {
|
||||
var fnA = pendingResolve;
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
fnA();
|
||||
}
|
||||
}
|
||||
|
||||
function onKey(e) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
if (blockUserDismiss) return;
|
||||
backdropDismiss();
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
var el = document.getElementById(OVERLAY_ID);
|
||||
if (!el) return;
|
||||
el.classList.remove("wesp-shell-dialog-overlay-closing");
|
||||
el.setAttribute("aria-hidden", "false");
|
||||
document.addEventListener("keydown", onKey);
|
||||
if (!prefersReducedMotion()) {
|
||||
var dlg = el.querySelector(".wesp-shell-dialog");
|
||||
if (dlg) {
|
||||
dlg.style.opacity = "0";
|
||||
dlg.style.transform = "scale(0.97) translateY(8px)";
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(function () {
|
||||
dlg.style.opacity = "";
|
||||
dlg.style.transform = "";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setDialogVariant(dialogEl, variant) {
|
||||
dialogEl.classList.remove("wesp-shell-dialog--danger", "wesp-shell-dialog--success");
|
||||
if (variant === "danger") dialogEl.classList.add("wesp-shell-dialog--danger");
|
||||
else if (variant === "success") dialogEl.classList.add("wesp-shell-dialog--success");
|
||||
}
|
||||
|
||||
function displayDialogMessage(message, opts) {
|
||||
const variant = opts && opts.variant;
|
||||
const fallback = opts && opts.fallback;
|
||||
if (variant === "danger" && global.WespUserMessages?.userFacingMessage) {
|
||||
return global.WespUserMessages.userFacingMessage(message, fallback || "Не удалось выполнить операцию. Попробуйте ещё раз.");
|
||||
}
|
||||
return message == null ? "" : String(message);
|
||||
}
|
||||
|
||||
function alert(message, opts) {
|
||||
opts = opts || {};
|
||||
return new Promise(function (resolve) {
|
||||
var overlay = ensureDom();
|
||||
var dialog = overlay.querySelector(".wesp-shell-dialog");
|
||||
var titleEl = document.getElementById("wespShellDialogTitle");
|
||||
var bodyEl = document.getElementById("wespShellDialogBody");
|
||||
var actions = document.getElementById("wespShellDialogActions");
|
||||
titleEl.textContent = opts.title != null ? String(opts.title) : "Сообщение";
|
||||
bodyEl.textContent = displayDialogMessage(message, opts);
|
||||
setDialogVariant(dialog, opts.variant || "info");
|
||||
actions.innerHTML = "";
|
||||
blockUserDismiss = !!opts.timerOnly;
|
||||
pendingMode = "alert";
|
||||
pendingResolve = resolve;
|
||||
if (!opts.timerOnly) {
|
||||
var ok = document.createElement("button");
|
||||
ok.type = "button";
|
||||
ok.className = "wesp-shell-dialog-btn wesp-shell-dialog-btn-primary";
|
||||
ok.textContent = opts.okText || "Понятно";
|
||||
ok.addEventListener("click", function () {
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
resolve();
|
||||
});
|
||||
actions.appendChild(ok);
|
||||
show();
|
||||
ok.focus();
|
||||
} else {
|
||||
dialog.setAttribute("tabindex", "-1");
|
||||
show();
|
||||
dialog.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirm(message, opts) {
|
||||
opts = opts || {};
|
||||
return new Promise(function (resolve) {
|
||||
blockUserDismiss = false;
|
||||
var overlay = ensureDom();
|
||||
var dialog = overlay.querySelector(".wesp-shell-dialog");
|
||||
var titleEl = document.getElementById("wespShellDialogTitle");
|
||||
var bodyEl = document.getElementById("wespShellDialogBody");
|
||||
var actions = document.getElementById("wespShellDialogActions");
|
||||
titleEl.textContent = opts.title != null ? String(opts.title) : "Подтверждение";
|
||||
bodyEl.textContent = message == null ? "" : String(message);
|
||||
setDialogVariant(dialog, opts.variant || "info");
|
||||
actions.innerHTML = "";
|
||||
var cancel = document.createElement("button");
|
||||
cancel.type = "button";
|
||||
cancel.className = "wesp-shell-dialog-btn";
|
||||
cancel.textContent = opts.cancelText || "Отмена";
|
||||
var ok = document.createElement("button");
|
||||
ok.type = "button";
|
||||
ok.className =
|
||||
"wesp-shell-dialog-btn " +
|
||||
(opts.danger ? "wesp-shell-dialog-btn-danger" : "wesp-shell-dialog-btn-primary");
|
||||
ok.textContent = opts.okText || "Да";
|
||||
cancel.addEventListener("click", function () {
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
resolve(false);
|
||||
});
|
||||
ok.addEventListener("click", function () {
|
||||
pendingResolve = null;
|
||||
pendingMode = null;
|
||||
hide();
|
||||
resolve(true);
|
||||
});
|
||||
actions.appendChild(cancel);
|
||||
actions.appendChild(ok);
|
||||
pendingMode = "confirm";
|
||||
pendingResolve = resolve;
|
||||
show();
|
||||
ok.focus();
|
||||
});
|
||||
}
|
||||
|
||||
global.WespDialog = {
|
||||
alert: alert,
|
||||
confirm: confirm,
|
||||
closeActive: closeActive,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Видимость Lab-элементов на zootech-страницах (components и др.).
|
||||
*/
|
||||
(function (global) {
|
||||
const HIDDEN_CLASS = "wesp-lab-access-hidden";
|
||||
|
||||
function ensureStyle() {
|
||||
if (document.getElementById("wespLabAccessStyle")) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "wespLabAccessStyle";
|
||||
style.textContent = `.${HIDDEN_CLASS}{display:none !important;}`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function setHidden(el, hidden) {
|
||||
if (!el) return;
|
||||
el.classList.toggle(HIDDEN_CLASS, hidden);
|
||||
if (hidden) {
|
||||
el.setAttribute("aria-hidden", "true");
|
||||
} else {
|
||||
el.removeAttribute("aria-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function apply(canLab) {
|
||||
ensureStyle();
|
||||
global.WESP_CAN_LAB = Boolean(canLab);
|
||||
document.documentElement.classList.toggle("wesp-no-lab", !canLab);
|
||||
|
||||
document.querySelectorAll("[data-wesp-lab-gated]").forEach((el) => {
|
||||
setHidden(el, !canLab);
|
||||
});
|
||||
|
||||
if (canLab) {
|
||||
global.WespLabComponentsNutrients?.init?.();
|
||||
}
|
||||
}
|
||||
|
||||
global.WespLabAccess = { apply };
|
||||
})(window);
|
||||
@@ -0,0 +1,869 @@
|
||||
const DEFAULT_BLUE_SRC = "/static/logo2.png";
|
||||
const LOGO_SRC_WIDTH = 2222;
|
||||
const LOGO_SRC_HEIGHT = 1024;
|
||||
/** ~28 строк сетки, как у старого logo2 (305px @ cell 11). */
|
||||
const INITIAL_CELL = 37;
|
||||
const DURATION_MS = 4750;
|
||||
const WAVE2_START_MS = 0.36 * 4500;
|
||||
const WAVE2_SPAN_MS = 0.34 * 6800;
|
||||
const WAVE2_START = WAVE2_START_MS / DURATION_MS;
|
||||
const WAVE2_SPAN = WAVE2_SPAN_MS / DURATION_MS;
|
||||
const WAVE2_END = (WAVE2_START_MS + WAVE2_SPAN_MS) / DURATION_MS;
|
||||
const HOLD_END = 0.94;
|
||||
const PIXEL_SCALE = WAVE2_START / 0.36;
|
||||
const WAVE0_START = 0.04 * PIXEL_SCALE;
|
||||
const WAVE0_SPAN = 0.20 * PIXEL_SCALE;
|
||||
const WAVE1_START = WAVE0_START + WAVE0_SPAN - 0.04 * PIXEL_SCALE;
|
||||
const WAVE1_SPAN = 0.20 * PIXEL_SCALE;
|
||||
const PIXEL_FADE = 0.012;
|
||||
const PIXEL_GAP = 1;
|
||||
const ASSEMBLY_FEATHER = 10;
|
||||
const COLOR_CROSSFADE = 0.024;
|
||||
const SHIMMER_MS = 1400;
|
||||
const SHIMMER_PROGRESS_START = 88;
|
||||
const BRAND_BLUE = [44, 123, 229];
|
||||
const BRAND_ORGANIC = [72, 129, 109];
|
||||
const BRAND_ORGANIC_HC = [47, 107, 87];
|
||||
const BRAND_WHITE = [238, 240, 243];
|
||||
const PIXEL_GRAY = [148, 152, 158];
|
||||
const BG = [246, 246, 244];
|
||||
const GRID_STEP = 10;
|
||||
|
||||
function shimmerDurationMs() {
|
||||
return SHIMMER_MS;
|
||||
}
|
||||
|
||||
function wave2EndMs(durationMs) {
|
||||
return WAVE2_END * durationMs;
|
||||
}
|
||||
|
||||
function animationEndMs(durationMs) {
|
||||
return Math.max(durationMs, wave2EndMs(durationMs) + SHIMMER_MS + 180);
|
||||
}
|
||||
|
||||
/** Фактическая длительность проигрывания (с учётом totalDurationMs). */
|
||||
function resolvePlaybackEndMs(durationMs, totalDurationMs) {
|
||||
const natural = animationEndMs(durationMs);
|
||||
if (totalDurationMs == null || !Number.isFinite(totalDurationMs)) {
|
||||
return natural;
|
||||
}
|
||||
return Math.min(natural, Math.max(400, totalDurationMs));
|
||||
}
|
||||
|
||||
export const LOGO_LOADER_DURATION_MS = DURATION_MS;
|
||||
export { animationEndMs, resolvePlaybackEndMs, drawShimmer, LOGO_SRC_WIDTH, LOGO_SRC_HEIGHT };
|
||||
|
||||
/** Буфер логотипа для navbar-shimmer (выравнивание слева, как object-position: left). */
|
||||
export function createNavLogoShimmerBuffer(img, slotW, slotH, inkRgb = [255, 255, 255]) {
|
||||
const srcW = img.naturalWidth || LOGO_SRC_WIDTH;
|
||||
const srcH = img.naturalHeight || LOGO_SRC_HEIGHT;
|
||||
const scale = Math.min(slotW / srcW, slotH / srcH);
|
||||
const drawWidth = srcW * scale;
|
||||
const drawHeight = srcH * scale;
|
||||
const layout = {
|
||||
offsetX: 0,
|
||||
offsetY: (slotH - drawHeight) / 2,
|
||||
drawWidth,
|
||||
drawHeight,
|
||||
};
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = slotW;
|
||||
canvas.height = slotH;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, slotW, slotH);
|
||||
ctx.drawImage(img, layout.offsetX, layout.offsetY, drawWidth, drawHeight);
|
||||
knockoutBackgroundFromLogo(canvas, slotW, slotH);
|
||||
if (inkRgb) recolorVisiblePixels(canvas, slotW, slotH, inkRgb);
|
||||
return { logoCanvas: canvas, layout };
|
||||
}
|
||||
|
||||
function prefersReducedMotion() {
|
||||
return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
|
||||
}
|
||||
|
||||
/** light | organic | dark — для цвета логотипа в анимации загрузки */
|
||||
function resolveLoaderTheme(options = {}) {
|
||||
const explicit = options.theme;
|
||||
if (explicit === "light" || explicit === "organic" || explicit === "dark") {
|
||||
return explicit;
|
||||
}
|
||||
const docTheme = document.documentElement.getAttribute("data-theme");
|
||||
if (docTheme === "light" || docTheme === "organic" || docTheme === "dark") {
|
||||
return docTheme;
|
||||
}
|
||||
try {
|
||||
const saved = localStorage.getItem("theme");
|
||||
if (saved === "light" || saved === "organic" || saved === "dark") return saved;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
if (options.darkTheme) return "dark";
|
||||
return "light";
|
||||
}
|
||||
|
||||
function prefersHighContrast() {
|
||||
const attr = document.documentElement.getAttribute("data-high-contrast");
|
||||
if (attr === "true") return true;
|
||||
if (attr === "false") return false;
|
||||
return globalThis.matchMedia?.("(prefers-contrast: more)")?.matches ?? false;
|
||||
}
|
||||
|
||||
function loaderAccentRgb(theme) {
|
||||
if (theme === "dark") return BRAND_WHITE;
|
||||
if (theme === "organic") return prefersHighContrast() ? BRAND_ORGANIC_HC : BRAND_ORGANIC;
|
||||
return BRAND_BLUE;
|
||||
}
|
||||
|
||||
function loaderUsesDarkShimmer(theme) {
|
||||
return theme === "dark";
|
||||
}
|
||||
|
||||
function loadImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
function isLogoInk(data, index) {
|
||||
const r = data[index];
|
||||
const g = data[index + 1];
|
||||
const b = data[index + 2];
|
||||
const a = data[index + 3];
|
||||
if (a < 20) return false;
|
||||
if (r + g + b < 80) return false;
|
||||
return b > 70 && b >= r && b >= g * 0.85;
|
||||
}
|
||||
|
||||
function isLogoBlueFringe(data, index) {
|
||||
const r = data[index];
|
||||
const g = data[index + 1];
|
||||
const b = data[index + 2];
|
||||
const a = data[index + 3];
|
||||
if (a < 1) return false;
|
||||
if (isLogoInk(data, index)) return true;
|
||||
return b > r + 5 && b > g - 2 && b > 68 && r + g + b < 700;
|
||||
}
|
||||
|
||||
function knockoutBackgroundFromLogo(canvas, width, height) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const imageData = ctx.getImageData(0, 0, width, height);
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i + 3] < 1) continue;
|
||||
if (isLogoBlueFringe(data, i)) continue;
|
||||
data[i + 3] = 0;
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
function recolorVisiblePixels(canvas, width, height, rgb) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const imageData = ctx.getImageData(0, 0, width, height);
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i + 3] < 1) continue;
|
||||
data[i] = rgb[0];
|
||||
data[i + 1] = rgb[1];
|
||||
data[i + 2] = rgb[2];
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
function cloneLogoBuffer(sourceCanvas, width, height, rgb = null) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(sourceCanvas, 0, 0);
|
||||
if (rgb) recolorVisiblePixels(canvas, width, height, rgb);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function drawLogoToBuffer(img, width, height, knockoutBackground = false) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const scale = Math.min(width / img.naturalWidth, height / img.naturalHeight);
|
||||
const drawWidth = img.naturalWidth * scale;
|
||||
const drawHeight = img.naturalHeight * scale;
|
||||
const offsetX = (width - drawWidth) / 2;
|
||||
const offsetY = (height - drawHeight) / 2;
|
||||
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
|
||||
if (knockoutBackground) knockoutBackgroundFromLogo(canvas, width, height);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function hashCell(col, row) {
|
||||
return ((col * 92837111) ^ (row * 689287499)) >>> 0;
|
||||
}
|
||||
|
||||
function easeOutQuad(t) {
|
||||
return 1 - (1 - t) * (1 - t);
|
||||
}
|
||||
|
||||
function easeInOutQuad(t) {
|
||||
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
|
||||
}
|
||||
|
||||
function rgbaString(r, g, b, a) {
|
||||
return `rgba(${r | 0}, ${g | 0}, ${b | 0}, ${a})`;
|
||||
}
|
||||
|
||||
function desaturateColor(r, g, b, amount = 0.72) {
|
||||
const gray = (r + g + b) / 3;
|
||||
return [
|
||||
r + (gray - r) * amount,
|
||||
g + (gray - g) * amount,
|
||||
b + (gray - b) * amount,
|
||||
];
|
||||
}
|
||||
|
||||
function lerpColor(c1, c2, t) {
|
||||
return [
|
||||
c1[0] + (c2[0] - c1[0]) * t,
|
||||
c1[1] + (c2[1] - c1[1]) * t,
|
||||
c1[2] + (c2[2] - c1[2]) * t,
|
||||
];
|
||||
}
|
||||
|
||||
function colorCss(color) {
|
||||
return `rgb(${color[0] | 0}, ${color[1] | 0}, ${color[2] | 0})`;
|
||||
}
|
||||
|
||||
function displayProgress(t, elapsedMs = null, durationMs = DURATION_MS) {
|
||||
const shimmerStartMs = wave2EndMs(durationMs);
|
||||
const shimmerEndMs = shimmerStartMs + shimmerDurationMs();
|
||||
|
||||
if (elapsedMs != null && elapsedMs >= shimmerStartMs) {
|
||||
if (elapsedMs >= shimmerEndMs) return 100;
|
||||
const shimmerP = (elapsedMs - shimmerStartMs) / shimmerDurationMs();
|
||||
return Math.round(
|
||||
SHIMMER_PROGRESS_START + shimmerP * (100 - SHIMMER_PROGRESS_START),
|
||||
);
|
||||
}
|
||||
|
||||
if (t >= WAVE2_END) return SHIMMER_PROGRESS_START;
|
||||
if (t >= WAVE2_START) {
|
||||
const p = (t - WAVE2_START) / (WAVE2_END - WAVE2_START);
|
||||
return Math.round(
|
||||
65 + Math.min(1, p) * (SHIMMER_PROGRESS_START - 65),
|
||||
);
|
||||
}
|
||||
if (t >= WAVE1_START) {
|
||||
const wave1End = WAVE1_START + WAVE1_SPAN;
|
||||
const p = Math.min(1, (t - WAVE1_START) / Math.max(0.001, wave1End - WAVE1_START));
|
||||
return Math.round(35 + p * 30);
|
||||
}
|
||||
if (t >= WAVE0_START) {
|
||||
const wave0End = WAVE0_START + WAVE0_SPAN;
|
||||
const p = Math.min(1, (t - WAVE0_START) / Math.max(0.001, wave0End - WAVE0_START));
|
||||
return Math.round(p * 35);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function progressBarColor(progress, loaderTheme = "light") {
|
||||
const accent = loaderAccentRgb(loaderTheme);
|
||||
if (progress >= 65) return colorCss(accent);
|
||||
if (progress >= 35) {
|
||||
const p = (progress - 35) / 30;
|
||||
return colorCss(lerpColor(PIXEL_GRAY, accent, p));
|
||||
}
|
||||
return colorCss(PIXEL_GRAY);
|
||||
}
|
||||
|
||||
function withLogoTransform(ctx, layout, t, drawFn) {
|
||||
drawFn();
|
||||
}
|
||||
|
||||
/** Фаза 0 — серые пиксели; фаза 1 — синие/белые; обе слева направо. */
|
||||
function buildLogoCells(logoData, logoWidth, logoHeight, cell, accentColor = null) {
|
||||
const cols = Math.ceil(logoWidth / cell);
|
||||
const rows = Math.ceil(logoHeight / cell);
|
||||
const colDenom = Math.max(1, cols - 1);
|
||||
const cells = [];
|
||||
|
||||
for (let row = 0; row < rows; row += 1) {
|
||||
for (let col = 0; col < cols; col += 1) {
|
||||
const cx = Math.min(logoWidth - 1, col * cell + Math.floor(cell / 2));
|
||||
const cy = Math.min(logoHeight - 1, row * cell + Math.floor(cell / 2));
|
||||
const index = (cy * logoWidth + cx) * 4;
|
||||
if (!isLogoInk(logoData, index)) continue;
|
||||
|
||||
const r = logoData[index];
|
||||
const g = logoData[index + 1];
|
||||
const b = logoData[index + 2];
|
||||
const noise = (hashCell(col, row) % 1000) / 1000;
|
||||
const sweep = Math.max(0, Math.min(1, col / colDenom + (noise - 0.5) * 0.028));
|
||||
const color = accentColor ?? [r, g, b];
|
||||
|
||||
cells.push({
|
||||
col,
|
||||
row,
|
||||
wave0At: WAVE0_START + sweep * WAVE0_SPAN,
|
||||
wave1At: WAVE1_START + sweep * WAVE1_SPAN,
|
||||
color,
|
||||
gray: desaturateColor(r, g, b),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
function wave2FrontX(t, layout) {
|
||||
if (t < WAVE2_START) {
|
||||
return layout.offsetX;
|
||||
}
|
||||
const progress = easeInOutQuad(Math.min(1, (t - WAVE2_START) / WAVE2_SPAN));
|
||||
return layout.offsetX + layout.drawWidth * progress;
|
||||
}
|
||||
|
||||
function drawBackground(ctx, width, height, layout, t, plain = false, transparent = false) {
|
||||
if (transparent) {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
if (plain) {
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
const cx = width / 2;
|
||||
const cy = layout ? layout.offsetY + layout.drawHeight / 2 : height * 0.44;
|
||||
const bgGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(width, height) * 0.72);
|
||||
bgGrad.addColorStop(0, "#FAFAF8");
|
||||
bgGrad.addColorStop(1, "#F0F0EE");
|
||||
ctx.fillStyle = bgGrad;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
const animating = t > 0 && t < HOLD_END;
|
||||
const logoCx = layout ? layout.offsetX + layout.drawWidth / 2 : cx;
|
||||
const logoCy = layout ? layout.offsetY + layout.drawHeight / 2 : cy;
|
||||
const focusRadius = layout
|
||||
? Math.max(layout.drawWidth, layout.drawHeight) * 0.72
|
||||
: Math.min(width, height) * 0.35;
|
||||
|
||||
for (let x = GRID_STEP / 2; x < width; x += GRID_STEP) {
|
||||
for (let y = GRID_STEP / 2; y < height; y += GRID_STEP) {
|
||||
let alpha = 0.055;
|
||||
if (layout && animating) {
|
||||
const dist = Math.hypot(x - logoCx, y - logoCy);
|
||||
if (dist < focusRadius) {
|
||||
alpha += 0.045 * (1 - dist / focusRadius);
|
||||
}
|
||||
}
|
||||
ctx.fillStyle = `rgba(26, 30, 28, ${alpha})`;
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const vig = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.28, cx, cy, Math.max(width, height) * 0.82);
|
||||
vig.addColorStop(0, "rgba(26, 30, 28, 0)");
|
||||
vig.addColorStop(1, "rgba(26, 30, 28, 0.07)");
|
||||
ctx.fillStyle = vig;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
function ensureLoaderChrome(root, hideChrome = false) {
|
||||
let canvas = root.querySelector(".wesp-logo-loader__canvas");
|
||||
let footer = root.querySelector(".wesp-logo-loader__footer");
|
||||
|
||||
if (!canvas) {
|
||||
canvas = document.createElement("canvas");
|
||||
canvas.className = "wesp-logo-loader__canvas";
|
||||
root.appendChild(canvas);
|
||||
}
|
||||
|
||||
if (!footer && !hideChrome) {
|
||||
footer = document.createElement("div");
|
||||
footer.className = "wesp-logo-loader__footer";
|
||||
footer.innerHTML = `
|
||||
<div class="wesp-logo-loader__footer-row">
|
||||
<span class="wesp-logo-loader__label">Загрузка</span>
|
||||
<span class="wesp-logo-loader__percent">00%</span>
|
||||
</div>
|
||||
<div class="wesp-logo-loader__bar">
|
||||
<div class="wesp-logo-loader__bar-fill"></div>
|
||||
</div>
|
||||
<p class="wesp-logo-loader__tagline" aria-live="polite"></p>`;
|
||||
root.appendChild(footer);
|
||||
}
|
||||
|
||||
return {
|
||||
canvas,
|
||||
percentEl: footer?.querySelector(".wesp-logo-loader__percent") ?? null,
|
||||
barFill: footer?.querySelector(".wesp-logo-loader__bar-fill") ?? null,
|
||||
taglineEl: footer?.querySelector(".wesp-logo-loader__tagline") ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function computeLogoLayout(viewport, logoWidth, logoHeight, embedded = false) {
|
||||
const maxWidth = viewport.w * (embedded ? 0.94 : 0.88);
|
||||
const maxHeight = viewport.h * (embedded ? 0.88 : 0.34);
|
||||
const scale = Math.min(maxWidth / logoWidth, maxHeight / logoHeight);
|
||||
const drawWidth = logoWidth * scale;
|
||||
const drawHeight = logoHeight * scale;
|
||||
const offsetX = (viewport.w - drawWidth) / 2;
|
||||
const offsetY = embedded
|
||||
? (viewport.h - drawHeight) / 2
|
||||
: viewport.h * 0.44 - drawHeight / 2;
|
||||
|
||||
return { scale, drawWidth, drawHeight, offsetX, offsetY };
|
||||
}
|
||||
|
||||
function drawSharpLogo(ctx, offBlue, layout, alpha = 1) {
|
||||
if (alpha <= 0) return;
|
||||
ctx.save();
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.drawImage(offBlue, layout.offsetX, layout.offsetY, layout.drawWidth, layout.drawHeight);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function pixelGrid(layout, cell, logoWidth, logoHeight) {
|
||||
const cols = Math.ceil(logoWidth / cell);
|
||||
const rows = Math.ceil(logoHeight / cell);
|
||||
return {
|
||||
originX: layout.offsetX,
|
||||
originY: layout.offsetY,
|
||||
cellW: layout.drawWidth / cols,
|
||||
cellH: layout.drawHeight / rows,
|
||||
};
|
||||
}
|
||||
|
||||
function cellRect(grid, col, row) {
|
||||
const x0 = grid.originX + col * grid.cellW;
|
||||
const y0 = grid.originY + row * grid.cellH;
|
||||
const x1 = grid.originX + (col + 1) * grid.cellW;
|
||||
const y1 = grid.originY + (row + 1) * grid.cellH;
|
||||
return {
|
||||
x: Math.round(x0),
|
||||
y: Math.round(y0),
|
||||
w: Math.max(1, Math.round(x1) - Math.round(x0)),
|
||||
h: Math.max(1, Math.round(y1) - Math.round(y0)),
|
||||
};
|
||||
}
|
||||
|
||||
function drawPixelBlock(ctx, rect, color, alpha) {
|
||||
if (alpha <= 0) return;
|
||||
const gap = Math.min(PIXEL_GAP, Math.max(0, rect.w - 1), Math.max(0, rect.h - 1));
|
||||
const inset = gap * 0.5;
|
||||
const x = rect.x + inset;
|
||||
const y = rect.y + inset;
|
||||
const w = Math.max(1, rect.w - gap);
|
||||
const h = Math.max(1, rect.h - gap);
|
||||
ctx.fillStyle = rgbaString(color[0], color[1], color[2], alpha);
|
||||
ctx.fillRect(x, y, w, h);
|
||||
}
|
||||
|
||||
function pixelAppear(t, startAt) {
|
||||
const appear = Math.min(1, (t - startAt) / PIXEL_FADE);
|
||||
return appear >= 0.7 ? 1 : appear;
|
||||
}
|
||||
|
||||
function resolvePixelColor(item, t) {
|
||||
if (t < item.wave1At) return item.gray;
|
||||
const blend = Math.min(1, (t - item.wave1At) / COLOR_CROSSFADE);
|
||||
if (blend >= 1) return item.color;
|
||||
return lerpColor(item.gray, item.color, blend);
|
||||
}
|
||||
|
||||
function resolvePixelAlpha(item, t) {
|
||||
if (t < item.wave0At) return 0;
|
||||
let alpha = pixelAppear(t, item.wave0At);
|
||||
if (t >= item.wave1At) {
|
||||
alpha = Math.max(alpha, pixelAppear(t, item.wave1At));
|
||||
}
|
||||
return alpha;
|
||||
}
|
||||
|
||||
function featherPixelAlpha(alpha, cellMid, frontX, feather) {
|
||||
if (cellMid <= frontX || cellMid >= frontX + feather) return alpha;
|
||||
return alpha * ((cellMid - frontX) / feather);
|
||||
}
|
||||
|
||||
function drawSharpLogoFeathered(ctx, logoCanvas, layout, frontX, feather) {
|
||||
const left = layout.offsetX;
|
||||
const top = layout.offsetY;
|
||||
const width = layout.drawWidth;
|
||||
const height = layout.drawHeight;
|
||||
const softStart = Math.max(left, frontX - feather);
|
||||
|
||||
if (softStart > left + 0.5) {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(left, top, softStart - left, height);
|
||||
ctx.clip();
|
||||
drawSharpLogo(ctx, logoCanvas, layout, 1);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
const bandLeft = softStart;
|
||||
const bandRight = Math.min(left + width, frontX);
|
||||
if (bandRight <= bandLeft + 0.5) return;
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(bandLeft, top, bandRight - bandLeft, height);
|
||||
ctx.clip();
|
||||
drawSharpLogo(ctx, logoCanvas, layout, 1);
|
||||
ctx.globalCompositeOperation = "destination-in";
|
||||
const grad = ctx.createLinearGradient(bandLeft, 0, bandRight, 0);
|
||||
grad.addColorStop(0, "rgba(255,255,255,1)");
|
||||
grad.addColorStop(1, "rgba(255,255,255,0)");
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(bandLeft, top, bandRight - bandLeft, height);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawShimmer(ctx, layout, logoCanvas, shimmerT, loaderTheme = "light") {
|
||||
const darkShimmer = loaderUsesDarkShimmer(loaderTheme);
|
||||
const bandW = layout.drawWidth * (darkShimmer ? 0.36 : 0.14);
|
||||
const x = layout.offsetX + (layout.drawWidth + bandW) * shimmerT - bandW;
|
||||
const left = layout.offsetX;
|
||||
const top = layout.offsetY;
|
||||
const width = layout.drawWidth;
|
||||
const height = layout.drawHeight;
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(left, top, width, height);
|
||||
ctx.clip();
|
||||
|
||||
if (darkShimmer) {
|
||||
const [gr, gg, gb] = PIXEL_GRAY;
|
||||
const grayWave = ctx.createLinearGradient(x, 0, x + bandW, 0);
|
||||
grayWave.addColorStop(0, "rgba(255,255,255,0)");
|
||||
grayWave.addColorStop(0.22, "rgba(255,255,255,0)");
|
||||
grayWave.addColorStop(0.5, `rgba(${gr},${gg},${gb},0.95)`);
|
||||
grayWave.addColorStop(0.78, "rgba(255,255,255,0)");
|
||||
grayWave.addColorStop(1, "rgba(255,255,255,0)");
|
||||
ctx.globalCompositeOperation = "source-atop";
|
||||
ctx.fillStyle = grayWave;
|
||||
ctx.fillRect(left, top, width, height);
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
|
||||
const peak = 0.72;
|
||||
const highlight = ctx.createLinearGradient(x, 0, x + bandW, 0);
|
||||
highlight.addColorStop(0, "rgba(255,255,255,0)");
|
||||
highlight.addColorStop(0.3, "rgba(255,255,255,0)");
|
||||
highlight.addColorStop(0.5, `rgba(255,255,255,${peak})`);
|
||||
highlight.addColorStop(0.7, "rgba(255,255,255,0)");
|
||||
highlight.addColorStop(1, "rgba(255,255,255,0)");
|
||||
ctx.globalCompositeOperation = "lighter";
|
||||
ctx.fillStyle = highlight;
|
||||
ctx.fillRect(left, top, width, height);
|
||||
ctx.globalCompositeOperation = "destination-in";
|
||||
ctx.drawImage(logoCanvas, left, top, width, height);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/** Фаза 0 — серые; фаза 1 — цветные; фаза 2 — плавная сборка по контуру надписи. */
|
||||
function drawWaves(ctx, layout, cell, cells, t, logoCanvas, logoWidth, logoHeight) {
|
||||
const frontX = wave2FrontX(t, layout);
|
||||
const sharpRight = layout.offsetX + layout.drawWidth;
|
||||
const grid = pixelGrid(layout, cell, logoWidth, logoHeight);
|
||||
const inWave2 = t >= WAVE2_START;
|
||||
|
||||
if (inWave2 && frontX > layout.offsetX + 1) {
|
||||
drawSharpLogoFeathered(ctx, logoCanvas, layout, frontX, ASSEMBLY_FEATHER);
|
||||
}
|
||||
|
||||
if (inWave2 && frontX >= sharpRight - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of cells) {
|
||||
if (t < item.wave0At) continue;
|
||||
|
||||
const rect = cellRect(grid, item.col, item.row);
|
||||
const cellMid = rect.x + rect.w * 0.5;
|
||||
if (inWave2 && cellMid < frontX - ASSEMBLY_FEATHER) continue;
|
||||
|
||||
let alpha = resolvePixelAlpha(item, t);
|
||||
if (inWave2) {
|
||||
alpha = featherPixelAlpha(alpha, cellMid, frontX, ASSEMBLY_FEATHER);
|
||||
}
|
||||
if (alpha <= 0) continue;
|
||||
|
||||
drawPixelBlock(ctx, rect, resolvePixelColor(item, t), alpha);
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgressChrome(
|
||||
percentEl,
|
||||
barFill,
|
||||
t,
|
||||
getExternalProgress,
|
||||
loaderTheme,
|
||||
elapsedMs = null,
|
||||
durationMs = DURATION_MS,
|
||||
) {
|
||||
let progress = displayProgress(t, elapsedMs, durationMs);
|
||||
const external = getExternalProgress();
|
||||
if (external != null) {
|
||||
progress = Math.max(progress, Math.min(100, Math.round(external * 100)));
|
||||
}
|
||||
|
||||
if (percentEl) percentEl.textContent = `${String(progress).padStart(2, "0")}%`;
|
||||
if (barFill) {
|
||||
barFill.style.width = `${progress}%`;
|
||||
barFill.style.background = progressBarColor(progress, loaderTheme);
|
||||
}
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
export async function mountWespLogoLoader(root, options = {}) {
|
||||
if (!root) {
|
||||
throw new Error("mountWespLogoLoader: root element is required");
|
||||
}
|
||||
|
||||
const blueSrc = options.blueSrc ?? options.whiteSrc ?? DEFAULT_BLUE_SRC;
|
||||
const initialCell = options.cell ?? options.pixel ?? INITIAL_CELL;
|
||||
const loop = options.loop !== false;
|
||||
const durationMs = options.durationMs ?? DURATION_MS;
|
||||
const totalDurationMs = options.totalDurationMs ?? null;
|
||||
const playbackEndMs = resolvePlaybackEndMs(durationMs, totalDurationMs);
|
||||
const embedded = !!options.embedded;
|
||||
const plainBackground = !!options.plainBackground;
|
||||
const transparentBackground = !!options.transparentBackground;
|
||||
const knockoutBackground = !!options.knockoutBackground;
|
||||
const loaderTheme = resolveLoaderTheme(options);
|
||||
const staticLogo = !!options.staticLogo;
|
||||
const hideChrome = !!options.hideChrome;
|
||||
const onComplete = typeof options.onComplete === "function" ? options.onComplete : null;
|
||||
const getExternalProgress = typeof options.progress === "function"
|
||||
? options.progress
|
||||
: () => (options.progress ?? null);
|
||||
|
||||
root.classList.add("wesp-logo-loader");
|
||||
if (embedded) root.classList.add("wesp-logo-loader--embedded");
|
||||
if (plainBackground) root.classList.add("wesp-logo-loader--plain");
|
||||
if (transparentBackground) root.classList.add("wesp-logo-loader--transparent");
|
||||
if (hideChrome) root.classList.add("wesp-logo-loader--no-chrome");
|
||||
|
||||
const { canvas, percentEl, barFill } = ensureLoaderChrome(root, hideChrome);
|
||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (transparentBackground) {
|
||||
canvas.style.background = "transparent";
|
||||
}
|
||||
|
||||
const blueImg = await loadImage(blueSrc);
|
||||
const logoWidth = blueImg.naturalWidth || LOGO_SRC_WIDTH;
|
||||
const logoHeight = blueImg.naturalHeight || LOGO_SRC_HEIGHT;
|
||||
const offBlue = drawLogoToBuffer(
|
||||
blueImg,
|
||||
logoWidth,
|
||||
logoHeight,
|
||||
knockoutBackground,
|
||||
);
|
||||
const accentRgb = loaderAccentRgb(loaderTheme);
|
||||
const displayLogo = loaderTheme === "light"
|
||||
? offBlue
|
||||
: cloneLogoBuffer(offBlue, logoWidth, logoHeight, accentRgb);
|
||||
const waveAccent = loaderTheme === "light" ? null : accentRgb;
|
||||
const logoData = offBlue.getContext("2d").getImageData(0, 0, logoWidth, logoHeight).data;
|
||||
const logoCells = buildLogoCells(
|
||||
logoData,
|
||||
logoWidth,
|
||||
logoHeight,
|
||||
initialCell,
|
||||
waveAccent,
|
||||
);
|
||||
|
||||
let viewport = { w: 0, h: 0, dpr: 1 };
|
||||
let layout = null;
|
||||
let rafId = 0;
|
||||
let start = performance.now();
|
||||
let disposed = false;
|
||||
let completedFired = false;
|
||||
|
||||
function finishIfDone(elapsedMs) {
|
||||
if (loop || elapsedMs < playbackEndMs || completedFired || !onComplete) return;
|
||||
completedFired = true;
|
||||
window.setTimeout(() => onComplete(), 120);
|
||||
}
|
||||
|
||||
function readViewportSize() {
|
||||
if (embedded) {
|
||||
const rect = root.getBoundingClientRect();
|
||||
return {
|
||||
w: Math.max(1, rect.width || root.clientWidth || 1),
|
||||
h: Math.max(1, rect.height || root.clientHeight || 1),
|
||||
};
|
||||
}
|
||||
return { w: globalThis.innerWidth, h: globalThis.innerHeight };
|
||||
}
|
||||
|
||||
function resize() {
|
||||
const dpr = Math.min(globalThis.devicePixelRatio || 1, 2);
|
||||
const size = readViewportSize();
|
||||
viewport = { w: size.w, h: size.h, dpr };
|
||||
|
||||
canvas.width = viewport.w * dpr;
|
||||
canvas.height = viewport.h * dpr;
|
||||
canvas.style.width = `${viewport.w}px`;
|
||||
canvas.style.height = `${viewport.h}px`;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
|
||||
layout = computeLogoLayout(viewport, logoWidth, logoHeight, embedded);
|
||||
}
|
||||
|
||||
function drawStaticLogo() {
|
||||
resize();
|
||||
drawBackground(ctx, viewport.w, viewport.h, layout, 1, plainBackground, transparentBackground);
|
||||
if (layout) drawSharpLogo(ctx, displayLogo, layout, 1);
|
||||
updateProgressChrome(
|
||||
percentEl,
|
||||
barFill,
|
||||
1,
|
||||
getExternalProgress,
|
||||
loaderTheme,
|
||||
animationEndMs(durationMs),
|
||||
durationMs,
|
||||
);
|
||||
finishIfDone(playbackEndMs);
|
||||
}
|
||||
|
||||
function drawAnimatedFrame(t, elapsedMs) {
|
||||
const shimmerMs = shimmerDurationMs();
|
||||
const shimmerStartMs = wave2EndMs(durationMs);
|
||||
|
||||
withLogoTransform(ctx, layout, t, () => {
|
||||
if (t >= WAVE2_END) {
|
||||
drawSharpLogo(ctx, displayLogo, layout, 1);
|
||||
if (
|
||||
elapsedMs >= shimmerStartMs
|
||||
&& elapsedMs < shimmerStartMs + shimmerMs
|
||||
) {
|
||||
drawShimmer(
|
||||
ctx,
|
||||
layout,
|
||||
displayLogo,
|
||||
(elapsedMs - shimmerStartMs) / shimmerMs,
|
||||
loaderTheme,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
drawWaves(ctx, layout, initialCell, logoCells, t, displayLogo, logoWidth, logoHeight);
|
||||
});
|
||||
}
|
||||
|
||||
function drawStaticIntroFrame(t) {
|
||||
drawBackground(ctx, viewport.w, viewport.h, layout, t, plainBackground, transparentBackground);
|
||||
if (!layout) return;
|
||||
const alpha = easeOutQuad(Math.min(1, t / 0.82));
|
||||
drawSharpLogo(ctx, displayLogo, layout, alpha);
|
||||
}
|
||||
|
||||
function cycleElapsedMs(elapsed) {
|
||||
if (!loop) return elapsed;
|
||||
return elapsed % durationMs;
|
||||
}
|
||||
|
||||
function drawFrame(now) {
|
||||
if (disposed) return;
|
||||
|
||||
const elapsed = now - start;
|
||||
const cycleMs = cycleElapsedMs(elapsed);
|
||||
const t = loop
|
||||
? cycleMs / durationMs
|
||||
: Math.min(1, elapsed / durationMs);
|
||||
const shimmerMs = shimmerDurationMs();
|
||||
const shimmerStartMs = wave2EndMs(durationMs);
|
||||
const inShimmer =
|
||||
cycleMs >= shimmerStartMs && cycleMs < shimmerStartMs + shimmerMs;
|
||||
|
||||
updateProgressChrome(
|
||||
percentEl,
|
||||
barFill,
|
||||
t,
|
||||
getExternalProgress,
|
||||
loaderTheme,
|
||||
cycleMs,
|
||||
durationMs,
|
||||
);
|
||||
|
||||
if (staticLogo) {
|
||||
drawStaticIntroFrame(t);
|
||||
finishIfDone(elapsed);
|
||||
if (loop || elapsed < playbackEndMs) {
|
||||
rafId = requestAnimationFrame(drawFrame);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
drawBackground(ctx, viewport.w, viewport.h, layout, t, plainBackground, transparentBackground);
|
||||
if (!layout) return;
|
||||
|
||||
if (t < HOLD_END || inShimmer) {
|
||||
drawAnimatedFrame(t, cycleMs);
|
||||
} else if (loop) {
|
||||
withLogoTransform(ctx, layout, t, () => {
|
||||
const fade = 1 - easeOutQuad((t - HOLD_END) / (1 - HOLD_END));
|
||||
drawSharpLogo(ctx, displayLogo, layout, fade);
|
||||
});
|
||||
} else {
|
||||
withLogoTransform(ctx, layout, t, () => {
|
||||
drawSharpLogo(ctx, displayLogo, layout, 1);
|
||||
});
|
||||
}
|
||||
|
||||
finishIfDone(elapsed);
|
||||
|
||||
if (loop || elapsed < playbackEndMs) {
|
||||
rafId = requestAnimationFrame(drawFrame);
|
||||
}
|
||||
}
|
||||
|
||||
resize();
|
||||
globalThis.addEventListener("resize", resize);
|
||||
|
||||
if (prefersReducedMotion() || staticLogo) {
|
||||
if (staticLogo) {
|
||||
rafId = requestAnimationFrame(drawFrame);
|
||||
} else {
|
||||
drawStaticLogo();
|
||||
}
|
||||
} else {
|
||||
rafId = requestAnimationFrame(drawFrame);
|
||||
}
|
||||
|
||||
return function disposeWespLogoLoader() {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
cancelAnimationFrame(rafId);
|
||||
globalThis.removeEventListener("resize", resize);
|
||||
root.classList.add("wesp-logo-loader--exiting");
|
||||
window.setTimeout(() => {
|
||||
root.classList.add("wesp-logo-loader--hidden");
|
||||
window.setTimeout(() => {
|
||||
if (root.isConnected) root.remove();
|
||||
}, 480);
|
||||
}, 520);
|
||||
};
|
||||
}
|
||||
|
||||
export async function showWespLogoLoader(options = {}) {
|
||||
const root = document.createElement("div");
|
||||
root.id = options.id ?? "wespBootLoader";
|
||||
root.className = "wesp-logo-loader";
|
||||
root.setAttribute("aria-busy", "true");
|
||||
root.setAttribute("aria-label", options.label ?? "Загрузка");
|
||||
document.body.appendChild(root);
|
||||
return mountWespLogoLoader(root, options);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Блик по буквам логотипа в navbar (тёмная тема) — тот же drawShimmer, что в wesp-logo-loader.
|
||||
*/
|
||||
(function () {
|
||||
const LOGO_SRC = "/static/logo2.png";
|
||||
const CYCLE_MS = 3000;
|
||||
const SHIMMER_MS = 1000;
|
||||
const START_DELAY_MS = 350;
|
||||
|
||||
const mounts = new WeakMap();
|
||||
|
||||
function isDarkTheme() {
|
||||
return document.documentElement.getAttribute("data-theme") === "dark";
|
||||
}
|
||||
|
||||
function prefersReducedMotion() {
|
||||
return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
|
||||
}
|
||||
|
||||
function loadImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => reject(new Error(`Failed to load: ${src}`));
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
function disposeMount(link) {
|
||||
const state = mounts.get(link);
|
||||
if (!state) return;
|
||||
state.disposed = true;
|
||||
if (state.rafId) cancelAnimationFrame(state.rafId);
|
||||
state.canvas?.remove();
|
||||
link.classList.remove("nav-logo-link--canvas-shimmer");
|
||||
mounts.delete(link);
|
||||
}
|
||||
|
||||
async function mountCanvasShimmer(link) {
|
||||
if (!isDarkTheme() || prefersReducedMotion()) return;
|
||||
disposeMount(link);
|
||||
|
||||
let logoModule;
|
||||
try {
|
||||
logoModule = await import("/static/js/wesp-logo-loader.js?v=2");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const { createNavLogoShimmerBuffer, drawShimmer } = logoModule;
|
||||
let img;
|
||||
try {
|
||||
img = await loadImage(LOGO_SRC);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.className = "nav-logo__shimmer-canvas";
|
||||
canvas.setAttribute("aria-hidden", "true");
|
||||
link.classList.add("nav-logo-link--canvas-shimmer");
|
||||
link.appendChild(canvas);
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
const state = {
|
||||
disposed: false,
|
||||
rafId: 0,
|
||||
canvas,
|
||||
logoCanvas: null,
|
||||
layout: null,
|
||||
startAt: performance.now() + START_DELAY_MS,
|
||||
lastW: 0,
|
||||
lastH: 0,
|
||||
};
|
||||
mounts.set(link, state);
|
||||
|
||||
function rebuildBuffer() {
|
||||
const w = Math.max(1, Math.round(link.clientWidth));
|
||||
const h = Math.max(1, Math.round(link.clientHeight));
|
||||
if (w === state.lastW && h === state.lastH && state.logoCanvas) return;
|
||||
|
||||
state.lastW = w;
|
||||
state.lastH = h;
|
||||
const dpr = Math.min(globalThis.devicePixelRatio || 1, 2);
|
||||
canvas.width = Math.round(w * dpr);
|
||||
canvas.height = Math.round(h * dpr);
|
||||
canvas.style.width = `${w}px`;
|
||||
canvas.style.height = `${h}px`;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
const built = createNavLogoShimmerBuffer(img, w, h, [255, 255, 255]);
|
||||
state.logoCanvas = built.logoCanvas;
|
||||
state.layout = built.layout;
|
||||
}
|
||||
|
||||
function tick(now) {
|
||||
if (state.disposed) return;
|
||||
if (!isDarkTheme()) {
|
||||
disposeMount(link);
|
||||
return;
|
||||
}
|
||||
|
||||
rebuildBuffer();
|
||||
|
||||
const w = state.lastW;
|
||||
const h = state.lastH;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const elapsed = now - state.startAt;
|
||||
if (elapsed >= 0 && state.logoCanvas && state.layout) {
|
||||
const cycle = elapsed % CYCLE_MS;
|
||||
if (cycle < SHIMMER_MS) {
|
||||
const shimmerT = cycle / SHIMMER_MS;
|
||||
ctx.drawImage(state.logoCanvas, 0, 0, w, h);
|
||||
drawShimmer(ctx, state.layout, state.logoCanvas, shimmerT, false);
|
||||
}
|
||||
}
|
||||
|
||||
state.rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
rebuildBuffer();
|
||||
state.rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (prefersReducedMotion()) return;
|
||||
|
||||
document
|
||||
.querySelectorAll(".mobile-navbar .nav-logo-link, .mobile-brand .nav-logo-link")
|
||||
.forEach((link) => {
|
||||
if (isDarkTheme()) {
|
||||
mountCanvasShimmer(link);
|
||||
} else {
|
||||
disposeMount(link);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onThemeChange() {
|
||||
init();
|
||||
}
|
||||
|
||||
window.WespNavLogoShimmer = { init, disposeMount };
|
||||
|
||||
const themeObserver = new MutationObserver(onThemeChange);
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme"],
|
||||
});
|
||||
|
||||
/* init вызывается из wesp-zootech-nav.js после вставки navbar и из wesp-page-enter после enter-done */
|
||||
})();
|
||||
@@ -0,0 +1,68 @@
|
||||
export const PRELOAD_FEED_DISPENSERS_KEY = "wesp_preload_feed_dispensers_v1";
|
||||
export const PRELOAD_FEED_DISPENSERS_MAX_AGE_MS = 120_000;
|
||||
export const LOGIN_TRANSITION_KEY = "wesp_login_transition_v1";
|
||||
export const LOGIN_TRANSITION_MAX_AGE_MS = 120_000;
|
||||
|
||||
function readStoredEntry(key, maxAgeMs) {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(key);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || Date.now() - parsed.storedAt > maxAgeMs) {
|
||||
sessionStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function markLoginTransition() {
|
||||
try {
|
||||
sessionStorage.setItem(LOGIN_TRANSITION_KEY, JSON.stringify({
|
||||
storedAt: Date.now(),
|
||||
}));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function storePreloadedApiPayload(key, payload) {
|
||||
try {
|
||||
sessionStorage.setItem(key, JSON.stringify({
|
||||
storedAt: Date.now(),
|
||||
payload,
|
||||
}));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function peekPreloadedApiPayload(key, maxAgeMs = PRELOAD_FEED_DISPENSERS_MAX_AGE_MS) {
|
||||
const entry = readStoredEntry(key, maxAgeMs);
|
||||
return entry?.payload ?? null;
|
||||
}
|
||||
|
||||
export function clearPreloadedApiPayload(key) {
|
||||
try {
|
||||
sessionStorage.removeItem(key);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function clearRecipesBootOverlay() {
|
||||
try {
|
||||
sessionStorage.removeItem(LOGIN_TRANSITION_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
document.documentElement.classList.remove("wesp-login-boot-pending");
|
||||
|
||||
const overlay = document.getElementById("wesp-recipes-boot");
|
||||
if (overlay) overlay.remove();
|
||||
|
||||
window.WespPageEnter?.reveal?.();
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Мягкая поэтапная анимация zootech-страниц: navbar → main → блоки.
|
||||
* html.wesp-page-enter-pending ставится inline в <head> до первого paint.
|
||||
*/
|
||||
(function () {
|
||||
const MAX_ITEMS = 8;
|
||||
let held = false;
|
||||
let revealed = false;
|
||||
|
||||
function prefersReducedMotion() {
|
||||
return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
|
||||
}
|
||||
|
||||
function isGridListPage() {
|
||||
const body = document.body;
|
||||
if (!body) return false;
|
||||
return (
|
||||
body.classList.contains("components-page") ||
|
||||
body.classList.contains("feed-dispensers-page") ||
|
||||
body.classList.contains("reports-page")
|
||||
);
|
||||
}
|
||||
|
||||
function markEnterItems() {
|
||||
const html = document.documentElement;
|
||||
if (isGridListPage()) {
|
||||
html.classList.add("wesp-page-enter-grid-page");
|
||||
return;
|
||||
}
|
||||
html.classList.remove("wesp-page-enter-grid-page");
|
||||
|
||||
const root = document.querySelector(".wesp-page-main");
|
||||
if (!root) return;
|
||||
|
||||
let items = Array.from(root.querySelectorAll("[data-wesp-enter]"));
|
||||
if (!items.length) {
|
||||
items = Array.from(root.children).filter((el) => el.nodeType === 1);
|
||||
}
|
||||
|
||||
items.slice(0, MAX_ITEMS).forEach((el, i) => {
|
||||
el.classList.add("wesp-page-enter-item");
|
||||
el.style.setProperty("--wesp-enter-i", String(i));
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-wesp-enter].app-footer, footer[data-wesp-enter]").forEach((el, i) => {
|
||||
if (el.classList.contains("wesp-page-enter-item")) return;
|
||||
el.classList.add("wesp-page-enter-item");
|
||||
el.style.setProperty("--wesp-enter-i", String(items.length + i));
|
||||
});
|
||||
}
|
||||
|
||||
function finalizeReveal() {
|
||||
if (revealed) return;
|
||||
revealed = true;
|
||||
const html = document.documentElement;
|
||||
html.classList.remove("wesp-page-enter-pending");
|
||||
html.classList.add("wesp-page-enter-ready");
|
||||
|
||||
let settled = false;
|
||||
let fallbackTimer = 0;
|
||||
|
||||
const settleDone = () => {
|
||||
if (settled || !html.classList.contains("wesp-page-enter-ready")) return;
|
||||
settled = true;
|
||||
if (fallbackTimer) window.clearTimeout(fallbackTimer);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
html.classList.remove("wesp-page-enter-ready");
|
||||
html.classList.add("wesp-page-enter-done");
|
||||
requestAnimationFrame(() => {
|
||||
globalThis.WespNavLogoShimmer?.init?.();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const items = document.querySelectorAll(".wesp-page-enter-item");
|
||||
let pending = items.length;
|
||||
if (pending === 0) {
|
||||
settleDone();
|
||||
return;
|
||||
}
|
||||
|
||||
const onAnimEnd = (event) => {
|
||||
if (event.animationName !== "wesp-enter-fade") return;
|
||||
pending -= 1;
|
||||
if (pending <= 0) settleDone();
|
||||
};
|
||||
|
||||
items.forEach((el) => {
|
||||
el.addEventListener("animationend", onAnimEnd, { once: true });
|
||||
});
|
||||
|
||||
const fallbackMs =
|
||||
80 + (MAX_ITEMS - 1) * 55 + 350 + 80;
|
||||
fallbackTimer = window.setTimeout(settleDone, fallbackMs);
|
||||
}
|
||||
|
||||
function revealPageMain() {
|
||||
if (revealed || held) return;
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!held && !revealed) finalizeReveal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function holdPageMain() {
|
||||
held = true;
|
||||
}
|
||||
|
||||
function releaseAndReveal() {
|
||||
held = false;
|
||||
if (!revealed) revealPageMain();
|
||||
}
|
||||
|
||||
function init() {
|
||||
markEnterItems();
|
||||
|
||||
if (prefersReducedMotion()) {
|
||||
finalizeReveal();
|
||||
document.documentElement.classList.add("wesp-page-enter-done");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!held) {
|
||||
revealPageMain();
|
||||
}
|
||||
}
|
||||
|
||||
window.WespPageEnter = {
|
||||
init,
|
||||
hold: holdPageMain,
|
||||
reveal: releaseAndReveal,
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,45 @@
|
||||
/* Синхронная ранняя установка data-theme + data-high-contrast + критические стили. */
|
||||
(function () {
|
||||
var KEY = "theme";
|
||||
var HC_KEY = "wespHighContrast";
|
||||
var saved = localStorage.getItem(KEY);
|
||||
var theme =
|
||||
saved === "dark" || saved === "light" || saved === "organic" ? saved : "dark";
|
||||
var root = document.documentElement;
|
||||
root.dataset.theme = theme;
|
||||
|
||||
var hcSaved = localStorage.getItem(HC_KEY);
|
||||
var highContrast =
|
||||
hcSaved === "1"
|
||||
? true
|
||||
: hcSaved === "0"
|
||||
? false
|
||||
: !!(window.matchMedia && window.matchMedia("(prefers-contrast: more)").matches);
|
||||
root.dataset.highContrast = highContrast ? "true" : "false";
|
||||
|
||||
var ORGANIC_LOGO_FILTER =
|
||||
"brightness(0) saturate(100%) invert(43%) sepia(19%) saturate(747%) " +
|
||||
"hue-rotate(118deg) brightness(92%) contrast(87%)";
|
||||
|
||||
var ORGANIC_LOGO_FILTER_HC =
|
||||
"brightness(0) saturate(100%) invert(28%) sepia(22%) saturate(900%) " +
|
||||
"hue-rotate(118deg) brightness(88%) contrast(96%)";
|
||||
|
||||
var organicLogoFilter =
|
||||
theme === "organic" && highContrast ? ORGANIC_LOGO_FILTER_HC : ORGANIC_LOGO_FILTER;
|
||||
|
||||
var css =
|
||||
'html{background:#f5f7fa}' +
|
||||
'html[data-theme="dark"]{background:#12181b}' +
|
||||
'html[data-theme="organic"]{background:#f6f6f4}' +
|
||||
'html[data-theme="organic"] .nav-brand .logo.logo--light-theme,' +
|
||||
'html[data-theme="organic"] .mobile-brand .logo.logo--light-theme{' +
|
||||
"filter:" +
|
||||
organicLogoFilter +
|
||||
"!important}";
|
||||
|
||||
var el = document.createElement("style");
|
||||
el.id = "wesp-theme-critical";
|
||||
el.textContent = css;
|
||||
document.head.appendChild(el);
|
||||
})();
|
||||
@@ -0,0 +1,143 @@
|
||||
/** Общая тема зоотех-страниц: localStorage + переключатель #wespThemePicker */
|
||||
export const THEME_KEY = "theme";
|
||||
export const HIGH_CONTRAST_KEY = "wespHighContrast";
|
||||
|
||||
export const THEMES = ["light", "organic", "dark"];
|
||||
|
||||
/** @deprecated Админка всегда ultra-dark; ключ оставлен для совместимости zootech-тем. */
|
||||
export const ADMIN_ULTRA_DARK_KEY = "wespAdminUltraDark";
|
||||
|
||||
const THEME_LABELS = {
|
||||
light: "Светлая",
|
||||
organic: "Зелёная",
|
||||
dark: "Тёмная",
|
||||
};
|
||||
|
||||
export function getTheme() {
|
||||
const saved = localStorage.getItem(THEME_KEY);
|
||||
if (THEMES.includes(saved)) return saved;
|
||||
return "dark";
|
||||
}
|
||||
|
||||
/** Ручной выбор в настройках; иначе — системный prefers-contrast. */
|
||||
export function resolveHighContrast() {
|
||||
try {
|
||||
const saved = localStorage.getItem(HIGH_CONTRAST_KEY);
|
||||
if (saved === "1") return true;
|
||||
if (saved === "0") return false;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return globalThis.matchMedia?.("(prefers-contrast: more)")?.matches ?? false;
|
||||
}
|
||||
|
||||
export function getHighContrast() {
|
||||
return resolveHighContrast();
|
||||
}
|
||||
|
||||
export function applyHighContrast(on) {
|
||||
document.documentElement.dataset.highContrast = on ? "true" : "false";
|
||||
}
|
||||
|
||||
export function setHighContrast(on) {
|
||||
try {
|
||||
localStorage.setItem(HIGH_CONTRAST_KEY, on ? "1" : "0");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
applyHighContrast(!!on);
|
||||
syncHighContrastUI();
|
||||
}
|
||||
|
||||
export function applyTheme(theme) {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
}
|
||||
|
||||
export function setTheme(theme) {
|
||||
if (!THEMES.includes(theme)) return;
|
||||
if (theme === "light" || theme === "organic") {
|
||||
try {
|
||||
localStorage.removeItem(ADMIN_ULTRA_DARK_KEY);
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
localStorage.setItem(THEME_KEY, theme);
|
||||
applyTheme(theme);
|
||||
syncThemePickerUI();
|
||||
}
|
||||
|
||||
function syncThemePickerUI() {
|
||||
const mount = document.getElementById("wespThemePicker");
|
||||
if (!mount) return;
|
||||
const current = getTheme();
|
||||
mount.querySelectorAll("[data-theme-value]").forEach((btn) => {
|
||||
const active = btn.dataset.themeValue === current;
|
||||
btn.classList.toggle("is-active", active);
|
||||
btn.setAttribute("aria-checked", active ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
function syncHighContrastUI() {
|
||||
const el = document.getElementById("wespHighContrastToggle");
|
||||
if (!el) return;
|
||||
const on = getHighContrast();
|
||||
el.checked = on;
|
||||
el.setAttribute("aria-checked", on ? "true" : "false");
|
||||
}
|
||||
|
||||
/** Сегментированный переключатель тем (3 варианта). */
|
||||
export function initThemePicker() {
|
||||
const mount = document.getElementById("wespThemePicker");
|
||||
if (!mount) return;
|
||||
|
||||
if (mount.dataset.bound !== "1") {
|
||||
mount.dataset.bound = "1";
|
||||
mount.classList.add("theme-picker");
|
||||
mount.innerHTML = THEMES.map(
|
||||
(id) =>
|
||||
`<button type="button" class="theme-picker__btn" data-theme-value="${id}" role="radio" aria-checked="false">${THEME_LABELS[id]}</button>`
|
||||
).join("");
|
||||
mount.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("[data-theme-value]");
|
||||
if (!btn) return;
|
||||
setTheme(btn.dataset.themeValue);
|
||||
});
|
||||
}
|
||||
|
||||
syncThemePickerUI();
|
||||
}
|
||||
|
||||
/** Переключатель усиленного контраста (под выбором темы). */
|
||||
export function initHighContrastToggle() {
|
||||
const el = document.getElementById("wespHighContrastToggle");
|
||||
if (!el) return;
|
||||
|
||||
if (el.dataset.bound !== "1") {
|
||||
el.dataset.bound = "1";
|
||||
el.addEventListener("change", () => setHighContrast(el.checked));
|
||||
}
|
||||
|
||||
syncHighContrastUI();
|
||||
}
|
||||
|
||||
/** @deprecated Используйте initThemePicker. Оставлен для обратной совместимости. */
|
||||
export function initThemeToggle() {
|
||||
if (document.getElementById("wespThemePicker")) {
|
||||
initThemePicker();
|
||||
return;
|
||||
}
|
||||
const el = document.getElementById("darkThemeToggle");
|
||||
if (!el) return;
|
||||
|
||||
if (el.dataset.bound === "1") {
|
||||
el.checked = document.documentElement.dataset.theme === "dark";
|
||||
return;
|
||||
}
|
||||
|
||||
el.dataset.bound = "1";
|
||||
el.checked = document.documentElement.dataset.theme === "dark";
|
||||
el.addEventListener("change", () => {
|
||||
setTheme(el.checked ? "dark" : "light");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* Фоновый poll статуса обновлений + баннер «Обновить / Позже».
|
||||
* WespUpdateNotifier.init({ dialog: WespDialog | WespKioskDialog });
|
||||
*/
|
||||
(function (global) {
|
||||
var DISMISS_KEY = "wesp_update_dismissed";
|
||||
var DEFAULT_POLL_MS = 300000;
|
||||
var PROGRESS_POLL_MS = 500;
|
||||
|
||||
var opts = {};
|
||||
var pollTimer = null;
|
||||
var progressTimer = null;
|
||||
var lastStatus = null;
|
||||
var lastProgress = null;
|
||||
|
||||
function ensureDom() {
|
||||
if (document.getElementById("wespUpdateBanner")) return;
|
||||
|
||||
var banner = document.createElement("div");
|
||||
banner.id = "wespUpdateBanner";
|
||||
banner.setAttribute("role", "region");
|
||||
banner.setAttribute("aria-label", "Доступно обновление");
|
||||
banner.innerHTML =
|
||||
'<div class="wesp-update-banner-inner">' +
|
||||
'<div class="wesp-update-banner-text" id="wespUpdateBannerText"></div>' +
|
||||
'<div class="wesp-update-banner-changelog" id="wespUpdateBannerChangelog"></div>' +
|
||||
'<div class="wesp-update-banner-actions">' +
|
||||
'<button type="button" class="wesp-update-banner-btn" id="wespUpdateBannerDetails">Подробнее</button>' +
|
||||
'<button type="button" class="wesp-update-banner-btn" id="wespUpdateBannerLater">Позже</button>' +
|
||||
'<button type="button" class="wesp-update-banner-btn wesp-update-banner-btn--primary" id="wespUpdateBannerInstall">Обновить</button>' +
|
||||
"</div></div>";
|
||||
document.body.appendChild(banner);
|
||||
|
||||
var overlay = document.createElement("div");
|
||||
overlay.id = "wespUpdateOverlay";
|
||||
overlay.setAttribute("aria-hidden", "true");
|
||||
overlay.innerHTML =
|
||||
'<div class="wesp-update-overlay-panel" role="alertdialog" aria-modal="true">' +
|
||||
'<h2 class="wesp-update-overlay-title" id="wespUpdateOverlayTitle">Идёт обновление</h2>' +
|
||||
'<p class="wesp-update-overlay-body" id="wespUpdateOverlayBody">Подготовка…</p>' +
|
||||
'<div class="wesp-update-progress-wrap" id="wespUpdateProgressWrap">' +
|
||||
'<div class="wesp-update-progress-track">' +
|
||||
'<div class="wesp-update-progress-bar" id="wespUpdateProgressBar"></div>' +
|
||||
"</div>" +
|
||||
'<div class="wesp-update-progress-meta">' +
|
||||
'<span class="wesp-update-progress-percent" id="wespUpdateProgressPercent">0%</span>' +
|
||||
'<span class="wesp-update-progress-detail" id="wespUpdateProgressDetail"></span>' +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
"</div>";
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
document.getElementById("wespUpdateBannerLater").addEventListener("click", dismissBanner);
|
||||
document.getElementById("wespUpdateBannerDetails").addEventListener("click", toggleDetails);
|
||||
document.getElementById("wespUpdateBannerInstall").addEventListener("click", onInstallClick);
|
||||
}
|
||||
|
||||
function isDismissed(version) {
|
||||
try {
|
||||
return sessionStorage.getItem(DISMISS_KEY) === String(version || "");
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function dismissBanner() {
|
||||
if (lastStatus && lastStatus.pending_version) {
|
||||
try {
|
||||
sessionStorage.setItem(DISMISS_KEY, String(lastStatus.pending_version));
|
||||
} catch (e) {}
|
||||
}
|
||||
hideBanner();
|
||||
}
|
||||
|
||||
function hideBanner() {
|
||||
var el = document.getElementById("wespUpdateBanner");
|
||||
if (el) {
|
||||
el.classList.remove("wesp-update-banner--visible", "wesp-update-banner--expanded");
|
||||
}
|
||||
}
|
||||
|
||||
function showBanner(status) {
|
||||
if (!status || !status.update_available || !status.pending_version) {
|
||||
hideBanner();
|
||||
return;
|
||||
}
|
||||
if (isDismissed(status.pending_version)) {
|
||||
hideBanner();
|
||||
return;
|
||||
}
|
||||
ensureDom();
|
||||
var el = document.getElementById("wespUpdateBanner");
|
||||
var text = document.getElementById("wespUpdateBannerText");
|
||||
var changelog = document.getElementById("wespUpdateBannerChangelog");
|
||||
if (!el || !text) return;
|
||||
|
||||
text.innerHTML =
|
||||
"Доступна версия <strong>" +
|
||||
escapeHtml(status.pending_version) +
|
||||
"</strong>" +
|
||||
(status.pending_name ? " — " + escapeHtml(status.pending_name) : "") +
|
||||
". Сейчас: <strong>" +
|
||||
escapeHtml(status.current_version || "—") +
|
||||
"</strong>.";
|
||||
|
||||
if (changelog) {
|
||||
changelog.textContent = status.pending_body || "";
|
||||
changelog.style.display = status.pending_body ? "" : "none";
|
||||
}
|
||||
|
||||
var detailsBtn = document.getElementById("wespUpdateBannerDetails");
|
||||
if (detailsBtn) {
|
||||
detailsBtn.style.display = status.pending_body ? "" : "none";
|
||||
}
|
||||
|
||||
el.classList.add("wesp-update-banner--visible");
|
||||
}
|
||||
|
||||
function toggleDetails() {
|
||||
var el = document.getElementById("wespUpdateBanner");
|
||||
if (el) el.classList.toggle("wesp-update-banner--expanded");
|
||||
}
|
||||
|
||||
function showInstallingOverlay(message, progress) {
|
||||
ensureDom();
|
||||
var overlay = document.getElementById("wespUpdateOverlay");
|
||||
if (overlay) {
|
||||
overlay.classList.add("wesp-update-overlay--visible");
|
||||
overlay.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
if (message) {
|
||||
var bodyEl = document.getElementById("wespUpdateOverlayBody");
|
||||
if (bodyEl) bodyEl.textContent = message;
|
||||
}
|
||||
applyProgressUi(progress);
|
||||
hideBanner();
|
||||
}
|
||||
|
||||
function hideInstallingOverlay() {
|
||||
var overlay = document.getElementById("wespUpdateOverlay");
|
||||
if (overlay) {
|
||||
overlay.classList.remove("wesp-update-overlay--visible");
|
||||
overlay.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
}
|
||||
|
||||
function applyProgressUi(progress) {
|
||||
if (!progress || typeof progress !== "object") return;
|
||||
lastProgress = progress;
|
||||
|
||||
var titleEl = document.getElementById("wespUpdateOverlayTitle");
|
||||
var bodyEl = document.getElementById("wespUpdateOverlayBody");
|
||||
var barEl = document.getElementById("wespUpdateProgressBar");
|
||||
var pctEl = document.getElementById("wespUpdateProgressPercent");
|
||||
var detailEl = document.getElementById("wespUpdateProgressDetail");
|
||||
|
||||
var pct = Number(progress.percent);
|
||||
if (!Number.isFinite(pct)) pct = 0;
|
||||
pct = Math.max(0, Math.min(100, Math.round(pct)));
|
||||
|
||||
if (titleEl && progress.target_version) {
|
||||
titleEl.textContent = "Обновление до версии " + progress.target_version;
|
||||
}
|
||||
if (bodyEl && progress.message) {
|
||||
bodyEl.textContent = progress.message;
|
||||
}
|
||||
if (barEl) {
|
||||
barEl.style.width = pct + "%";
|
||||
barEl.setAttribute("aria-valuenow", String(pct));
|
||||
}
|
||||
if (pctEl) pctEl.textContent = pct + "%";
|
||||
if (detailEl) {
|
||||
detailEl.textContent = progress.detail || "";
|
||||
detailEl.style.display = progress.detail ? "" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
function stopProgressPolling() {
|
||||
if (progressTimer) {
|
||||
clearInterval(progressTimer);
|
||||
progressTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startProgressPolling() {
|
||||
stopProgressPolling();
|
||||
progressTimer = setInterval(function () {
|
||||
fetchStatus().then(function (status) {
|
||||
if (!status) return;
|
||||
if (status.update_progress) {
|
||||
applyProgressUi(status.update_progress);
|
||||
} else if (status.is_updating) {
|
||||
applyProgressUi({
|
||||
message: "Выполняется обновление…",
|
||||
percent: lastProgress ? lastProgress.percent : 5,
|
||||
});
|
||||
}
|
||||
});
|
||||
}, PROGRESS_POLL_MS);
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function fetchStatus() {
|
||||
var url = opts.statusUrl || "/api/updates/status";
|
||||
return fetch(url, { credentials: "same-origin", cache: "no-store" }).then(function (r) {
|
||||
if (r.status === 401 || r.status === 403) return null;
|
||||
return r.json().catch(function () {
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function applyStatus(status) {
|
||||
if (!status || typeof status !== "object") return;
|
||||
lastStatus = status;
|
||||
if (status.update_available && status.pending_version) {
|
||||
try {
|
||||
var dismissed = sessionStorage.getItem(DISMISS_KEY);
|
||||
if (dismissed && dismissed !== String(status.pending_version)) {
|
||||
sessionStorage.removeItem(DISMISS_KEY);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
showBanner(status);
|
||||
}
|
||||
|
||||
function pollOnce() {
|
||||
return fetchStatus().then(function (status) {
|
||||
applyStatus(status);
|
||||
});
|
||||
}
|
||||
|
||||
function onInstallClick() {
|
||||
var dialog = opts.dialog;
|
||||
if (!dialog || typeof dialog.confirm !== "function") {
|
||||
if (!global.confirm("Система перезапустится. Продолжить?")) return;
|
||||
doInstall();
|
||||
return;
|
||||
}
|
||||
var msg =
|
||||
"Будет загружена и установлена версия " +
|
||||
(lastStatus && lastStatus.pending_version ? lastStatus.pending_version : "") +
|
||||
".\n\nСистема перезапустится. Продолжить?";
|
||||
dialog
|
||||
.confirm(msg, {
|
||||
title: "Обновление WESP",
|
||||
okText: "Обновить",
|
||||
cancelText: "Отмена",
|
||||
danger: true,
|
||||
})
|
||||
.then(function (ok) {
|
||||
if (ok) doInstall();
|
||||
});
|
||||
}
|
||||
|
||||
function waitForServerAndReload() {
|
||||
var url = opts.statusUrl || "/api/updates/status";
|
||||
var attempts = 0;
|
||||
var maxAttempts = 90;
|
||||
var intervalMs = 2000;
|
||||
|
||||
showInstallingOverlay(
|
||||
"Обновление установлено. Ожидаем перезапуск сервера — страница обновится автоматически.",
|
||||
{ message: "Ожидание перезапуска сервера…", percent: 100, detail: null }
|
||||
);
|
||||
|
||||
function scheduleNext() {
|
||||
if (attempts >= maxAttempts) {
|
||||
var bodyEl = document.getElementById("wespUpdateOverlayBody");
|
||||
if (bodyEl) {
|
||||
bodyEl.innerHTML =
|
||||
"Сервер долго не отвечает. Если перезапуск уже завершился, " +
|
||||
'<button type="button" class="wesp-update-banner-btn wesp-update-banner-btn--primary" id="wespUpdateManualReload">Обновить страницу</button>';
|
||||
var btn = document.getElementById("wespUpdateManualReload");
|
||||
if (btn) btn.addEventListener("click", function () { global.location.reload(); });
|
||||
}
|
||||
return;
|
||||
}
|
||||
setTimeout(tick, intervalMs);
|
||||
}
|
||||
|
||||
function showRollbackOverlay(state) {
|
||||
var ver = (state && (state.previous_version || state.message)) || "—";
|
||||
var msg =
|
||||
(state && state.message) ||
|
||||
"Обновление отменено. Восстановлена предыдущая версия.";
|
||||
showInstallingOverlay(msg, {
|
||||
message: msg,
|
||||
percent: 100,
|
||||
target_version: ver,
|
||||
});
|
||||
var bar = document.getElementById("wespUpdateProgressBar");
|
||||
var panel = document.querySelector("#wespUpdateOverlay .wesp-update-overlay-panel");
|
||||
if (bar) bar.classList.add("wesp-update-progress-bar--error");
|
||||
if (panel) panel.classList.add("wesp-update-overlay-panel--rollback");
|
||||
var bodyEl = document.getElementById("wespUpdateOverlayBody");
|
||||
if (bodyEl) {
|
||||
bodyEl.innerHTML =
|
||||
escapeHtml(msg) +
|
||||
' <button type="button" class="wesp-update-banner-btn wesp-update-banner-btn--primary" id="wespUpdateManualReload">Обновить страницу</button>';
|
||||
var btn = document.getElementById("wespUpdateManualReload");
|
||||
if (btn) btn.addEventListener("click", function () { global.location.reload(); });
|
||||
}
|
||||
}
|
||||
|
||||
function tick() {
|
||||
attempts += 1;
|
||||
applyProgressUi({
|
||||
message: "Ожидание перезапуска сервера…",
|
||||
percent: 100,
|
||||
detail: "~" + attempts * 2 + " с",
|
||||
});
|
||||
fetch(url, { credentials: "same-origin", cache: "no-store" })
|
||||
.then(function (r) {
|
||||
return r.json().catch(function () { return null; }).then(function (body) {
|
||||
return { ok: r.ok, status: r.status, body: body };
|
||||
});
|
||||
})
|
||||
.then(function (res) {
|
||||
var st = res.body && res.body.last_update_state;
|
||||
if (st && st.status === "rolled_back") {
|
||||
showRollbackOverlay(st);
|
||||
return;
|
||||
}
|
||||
if (res.ok || res.status === 401 || res.status === 403) {
|
||||
global.location.reload();
|
||||
return;
|
||||
}
|
||||
scheduleNext();
|
||||
})
|
||||
.catch(function () {
|
||||
scheduleNext();
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(tick, 3000);
|
||||
}
|
||||
|
||||
function doInstall() {
|
||||
var targetVersion = lastStatus && lastStatus.pending_version ? lastStatus.pending_version : "";
|
||||
showInstallingOverlay("Подготовка к обновлению…", {
|
||||
message: "Подготовка к обновлению…",
|
||||
percent: 0,
|
||||
target_version: targetVersion,
|
||||
});
|
||||
startProgressPolling();
|
||||
|
||||
var url = opts.installUrl || "/api/updates/install";
|
||||
fetch(url, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then(function (r) {
|
||||
return r.json().catch(function () {
|
||||
return {};
|
||||
}).then(function (body) {
|
||||
return { ok: r.ok, body: body };
|
||||
});
|
||||
})
|
||||
.then(function (res) {
|
||||
stopProgressPolling();
|
||||
if (!res.ok) {
|
||||
hideInstallingOverlay();
|
||||
var errMsg = (res.body && (res.body.error || res.body.message)) || "Не удалось установить обновление";
|
||||
if (opts.dialog && opts.dialog.alert) {
|
||||
opts.dialog.alert(errMsg, { title: "Ошибка обновления", variant: "danger" });
|
||||
} else {
|
||||
global.alert(errMsg);
|
||||
}
|
||||
pollOnce();
|
||||
return;
|
||||
}
|
||||
waitForServerAndReload();
|
||||
})
|
||||
.catch(function () {
|
||||
stopProgressPolling();
|
||||
hideInstallingOverlay();
|
||||
if (opts.dialog && opts.dialog.alert) {
|
||||
opts.dialog.alert("Сетевая ошибка при установке обновления.", { title: "Ошибка", variant: "danger" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function init(options) {
|
||||
opts = options || {};
|
||||
if (!opts.dialog) {
|
||||
opts.dialog = global.WespDialog || global.WespKioskDialog || null;
|
||||
}
|
||||
ensureDom();
|
||||
pollOnce();
|
||||
var interval = Number(opts.pollIntervalMs);
|
||||
if (!Number.isFinite(interval) || interval < 60000) interval = DEFAULT_POLL_MS;
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
pollTimer = setInterval(pollOnce, interval);
|
||||
}
|
||||
|
||||
global.WespUpdateNotifier = {
|
||||
init: init,
|
||||
pollOnce: pollOnce,
|
||||
dismissBanner: dismissBanner,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
@@ -0,0 +1,61 @@
|
||||
/** Правила пароля — как в админке и setup wizard. Логин без ограничений по символам. */
|
||||
export const DEFAULT_PASSWORD_MIN_LEN = 8;
|
||||
export const LOGIN_MAX_LEN = 64;
|
||||
export const PASSWORD_COMPLEXITY_MSG =
|
||||
"Пароль не должен состоять только из цифр или только из букв. Смешайте буквы и цифры.";
|
||||
|
||||
export function validateNewUserCredentials({
|
||||
login,
|
||||
password,
|
||||
confirmPassword,
|
||||
passwordMinLen = DEFAULT_PASSWORD_MIN_LEN,
|
||||
label = "",
|
||||
}) {
|
||||
const prefix = label ? `${label}: ` : "";
|
||||
const trimmedLogin = String(login || "").trim();
|
||||
if (!trimmedLogin) return `${prefix}укажите логин.`;
|
||||
if (trimmedLogin.length > LOGIN_MAX_LEN) {
|
||||
return `${prefix}логин не длиннее ${LOGIN_MAX_LEN} символов.`;
|
||||
}
|
||||
if (!password) return `${prefix}укажите пароль.`;
|
||||
if (password !== confirmPassword) return `${prefix}пароли не совпадают.`;
|
||||
if (password.length < passwordMinLen) {
|
||||
return `${prefix}пароль не короче ${passwordMinLen} символов.`;
|
||||
}
|
||||
if (/^\d+$/.test(password) || /^[a-zA-Z]+$/.test(password)) {
|
||||
return `${prefix}${PASSWORD_COMPLEXITY_MSG}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateCredentialChange({
|
||||
oldLogin,
|
||||
oldPassword,
|
||||
newLogin,
|
||||
newPassword,
|
||||
confirmPassword,
|
||||
passwordMinLen = DEFAULT_PASSWORD_MIN_LEN,
|
||||
}) {
|
||||
if (!oldLogin || !oldPassword) {
|
||||
return "Укажите текущий логин и пароль";
|
||||
}
|
||||
return validateNewUserCredentials({
|
||||
login: newLogin,
|
||||
password: newPassword,
|
||||
confirmPassword,
|
||||
passwordMinLen,
|
||||
});
|
||||
}
|
||||
|
||||
export function ensureCredentialsFormHints() {}
|
||||
|
||||
if (typeof globalThis !== "undefined") {
|
||||
globalThis.WespUserCredentials = {
|
||||
DEFAULT_PASSWORD_MIN_LEN,
|
||||
LOGIN_MAX_LEN,
|
||||
PASSWORD_COMPLEXITY_MSG,
|
||||
validateCredentialChange,
|
||||
validateNewUserCredentials,
|
||||
ensureCredentialsFormHints,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Безопасные тексты для пользователя: без HTTP-кодов, stack trace и прочей техники.
|
||||
*/
|
||||
(function (global) {
|
||||
const DEFAULT_ERROR_MESSAGE = "Не удалось выполнить операцию. Попробуйте ещё раз.";
|
||||
|
||||
const TECHNICAL_MESSAGE_PATTERNS = [
|
||||
/\bHTTP\s*\d{3}\b/i,
|
||||
/^\d{3}$/,
|
||||
/ошибка\s*\d{3}/i,
|
||||
/status:\s*\d+/i,
|
||||
/Failed to fetch/i,
|
||||
/NetworkError/i,
|
||||
/Load failed/i,
|
||||
/SyntaxError/i,
|
||||
/TypeError/i,
|
||||
/ReferenceError/i,
|
||||
/RangeError/i,
|
||||
/Traceback/i,
|
||||
/\bException\b/i,
|
||||
/UnicodeEncodeError/i,
|
||||
/Internal Server Error/i,
|
||||
/<html[\s>]/i,
|
||||
/<!DOCTYPE/i,
|
||||
/^Error:/i,
|
||||
/\.py:\d+/i,
|
||||
/\.js:\d+/i,
|
||||
/at\s+.+\(.+:\d+:\d+\)/,
|
||||
/Unexpected token/i,
|
||||
/SQLALCHEMY|werkzeug|flask/i,
|
||||
/date_from|date_to|YYYY-MM-DD/i,
|
||||
/\bapi\/[a-z0-9_/-]+/i,
|
||||
/zootechnician|warehouse_keeper|recipient/i,
|
||||
/Content-Disposition/i,
|
||||
/latin-1/i,
|
||||
/ordinal not in range/i,
|
||||
/GET\s+\/api\//i,
|
||||
/sync-diagnostics/i,
|
||||
/\bfail\b$/i,
|
||||
/chat fail|ping fail|summary fail|diagnostics fail|requeue fail|restart fail|runtime fail|push fail/i,
|
||||
/status fail/i,
|
||||
/no response/i,
|
||||
];
|
||||
|
||||
function plainText(message) {
|
||||
if (message instanceof Error) return "";
|
||||
if (message && typeof message === "object") {
|
||||
if (Array.isArray(message.parts)) {
|
||||
return message.parts.map((p) => (p && p.text != null ? String(p.text) : "")).join("");
|
||||
}
|
||||
if (message.text != null) return String(message.text);
|
||||
}
|
||||
return message == null ? "" : String(message);
|
||||
}
|
||||
|
||||
function isTechnicalMessage(message) {
|
||||
const text = plainText(message).trim();
|
||||
if (!text) return true;
|
||||
return TECHNICAL_MESSAGE_PATTERNS.some((pattern) => pattern.test(text));
|
||||
}
|
||||
|
||||
function userFacingMessage(message, fallback) {
|
||||
const fb = (fallback && String(fallback).trim()) || DEFAULT_ERROR_MESSAGE;
|
||||
if (message instanceof Error) return fb;
|
||||
const text = plainText(message).trim();
|
||||
if (!text || isTechnicalMessage(text)) return fb;
|
||||
return text;
|
||||
}
|
||||
|
||||
function messageFromResponseBody(body, fallback) {
|
||||
if (!body || typeof body !== "object") {
|
||||
return userFacingMessage(null, fallback);
|
||||
}
|
||||
return userFacingMessage(body.message || body.error || null, fallback);
|
||||
}
|
||||
|
||||
function safe(message, fallback) {
|
||||
return userFacingMessage(message, fallback);
|
||||
}
|
||||
|
||||
global.WespUserMessages = {
|
||||
DEFAULT_ERROR_MESSAGE,
|
||||
plainText,
|
||||
isTechnicalMessage,
|
||||
userFacingMessage,
|
||||
messageFromResponseBody,
|
||||
safe,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
@@ -96,14 +96,31 @@
|
||||
const mobileMenuOverlay = document.getElementById("mobileMenuOverlay");
|
||||
const closeMenu = document.getElementById("closeMenu");
|
||||
if (!hamburgerMenu || !mobileMenuOverlay) return;
|
||||
if (mobileMenuOverlay.dataset.menuWired === "1") return;
|
||||
mobileMenuOverlay.dataset.menuWired = "1";
|
||||
|
||||
if (mobileMenuOverlay.parentElement !== document.body) {
|
||||
document.body.appendChild(mobileMenuOverlay);
|
||||
}
|
||||
|
||||
const syncMobileUserInfo = () => {
|
||||
const userInfo = document.getElementById("userInfo");
|
||||
const mobileUserInfo = document.getElementById("mobileUserInfo");
|
||||
if (userInfo && mobileUserInfo) {
|
||||
mobileUserInfo.textContent = userInfo.textContent?.trim() || "";
|
||||
}
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
syncMobileUserInfo();
|
||||
mobileMenuOverlay.classList.add("active");
|
||||
hamburgerMenu.classList.add("active");
|
||||
document.body.classList.add("mobile-menu-open");
|
||||
};
|
||||
const close = () => {
|
||||
mobileMenuOverlay.classList.remove("active");
|
||||
hamburgerMenu.classList.remove("active");
|
||||
document.body.classList.remove("mobile-menu-open");
|
||||
};
|
||||
|
||||
hamburgerMenu.addEventListener("click", open);
|
||||
@@ -111,6 +128,14 @@
|
||||
mobileMenuOverlay.addEventListener("click", (e) => {
|
||||
if (e.target === mobileMenuOverlay) close();
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && mobileMenuOverlay.classList.contains("active")) close();
|
||||
});
|
||||
document.querySelectorAll(".mobile-menu-item").forEach((item) => {
|
||||
item.addEventListener("click", () => setTimeout(close, 300));
|
||||
});
|
||||
|
||||
window.WespZootechNavCloseMobileMenu = close;
|
||||
}
|
||||
|
||||
async function fetchCanLab() {
|
||||
|
||||
@@ -15,6 +15,31 @@
|
||||
let hubEl = null;
|
||||
let planMountEl = null;
|
||||
let multiserverMountEl = null;
|
||||
let orchContextLoaded = false;
|
||||
|
||||
async function ensureOrchContext() {
|
||||
if (orchContextLoaded || global.__WESP_ORCH__?.enterpriseId) {
|
||||
orchContextLoaded = true;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch("/api/v1/orchestrator/context");
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
global.__WESP_ORCH__ = Object.assign(global.__WESP_ORCH__ || {}, {
|
||||
enterpriseId: data.enterpriseId || "",
|
||||
configured: !!data.configured,
|
||||
reachable: !!data.reachable,
|
||||
upstream: data.upstream || "",
|
||||
hubSiteId: data.hubSiteId || "",
|
||||
});
|
||||
}
|
||||
} catch (_err) {
|
||||
/* hub may run without orchestrator */
|
||||
} finally {
|
||||
orchContextLoaded = true;
|
||||
}
|
||||
}
|
||||
let titleEl = null;
|
||||
let readAllBtn = null;
|
||||
let backBtn = null;
|
||||
@@ -789,7 +814,7 @@
|
||||
showModal();
|
||||
}
|
||||
|
||||
function openMultiserver() {
|
||||
async function openMultiserver() {
|
||||
ensureModal();
|
||||
updateHeader("multiserver");
|
||||
hubEl.hidden = true;
|
||||
@@ -797,6 +822,7 @@
|
||||
planMountEl.hidden = true;
|
||||
multiserverMountEl.hidden = false;
|
||||
destroyDailyPlanPanel();
|
||||
await ensureOrchContext();
|
||||
const enterpriseId = global.__WESP_ORCH__?.enterpriseId || "";
|
||||
if (global.WespMultiserverPanel?.mount) {
|
||||
global.WespMultiserverPanel.mount(multiserverMountEl, enterpriseId);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Вход</title>
|
||||
<script src="/static/js/wesp-theme-boot.js"></script>
|
||||
<link href="/static/css/wesp-zootech-layout.css?v=16" rel="stylesheet"> <link href="/static/css/inter-fonts.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/css/zootechnician-login.css">
|
||||
<link rel="stylesheet" href="/static/css/wesp-logo-loader.css">
|
||||
<link href="/static/css/wesp-zootech-theme-organic.css?v=6" rel="stylesheet">
|
||||
</head>
|
||||
<body class="zootech-login-body">
|
||||
<div class="z-login-page">
|
||||
<div class="z-login-card">
|
||||
<div class="z-login-form-panel">
|
||||
<h1 class="z-login-title">Вход</h1>
|
||||
|
||||
<div id="errorMessage" class="z-login-msg z-login-msg--error" role="alert"></div>
|
||||
|
||||
<form class="z-login-form" id="loginForm">
|
||||
<div class="z-login-field z-login-field--with-icon">
|
||||
<span class="z-login-field-icon" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
</span>
|
||||
<input type="text" id="loginInput" class="z-login-input" placeholder="Логин"
|
||||
autocomplete="username" required aria-label="Логин">
|
||||
</div>
|
||||
<div class="z-login-field z-login-field--with-icon">
|
||||
<span class="z-login-field-icon" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
</span>
|
||||
<input type="password" id="passwordInput" class="z-login-input" placeholder="Пароль"
|
||||
autocomplete="current-password" required aria-label="Пароль">
|
||||
</div>
|
||||
|
||||
<button type="submit" id="authButton" class="z-login-submit">
|
||||
<span id="buttonText">Войти</span>
|
||||
<div id="loadingSpinner" class="z-login-spinner" aria-hidden="true"></div>
|
||||
</button>
|
||||
|
||||
<label class="z-login-remember">
|
||||
<input type="checkbox" id="rememberInput" class="z-login-switch-input">
|
||||
<span class="z-login-switch-ui" aria-hidden="true"></span>
|
||||
<span class="z-login-remember-text">Запомнить вход</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/static/js/pages/login-page.js?v=4"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Комптон — demo загрузки</title>
|
||||
<link rel="stylesheet" href="/static/css/wesp-logo-loader.css">
|
||||
</head>
|
||||
<body class="wesp-logo-loader-demo">
|
||||
<p class="wesp-logo-loader-demo__hint">Серые пиксели → синие → сборка · прогресс по фазам</p>
|
||||
|
||||
<div class="wesp-logo-loader-demo__panel">
|
||||
<button type="button" class="wesp-logo-loader-demo__btn" id="replayBtn">Показать снова</button>
|
||||
<button type="button" class="wesp-logo-loader-demo__btn" id="hideBtn">Скрыть (exit-анимация)</button>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { showWespLogoLoader } from "/static/js/wesp-logo-loader.js";
|
||||
|
||||
let dispose = null;
|
||||
|
||||
async function play() {
|
||||
if (dispose) {
|
||||
dispose();
|
||||
dispose = null;
|
||||
}
|
||||
dispose = await showWespLogoLoader({ id: "wespBootLoaderDemo" });
|
||||
}
|
||||
|
||||
document.getElementById("replayBtn").addEventListener("click", () => {
|
||||
play();
|
||||
});
|
||||
|
||||
document.getElementById("hideBtn").addEventListener("click", () => {
|
||||
if (dispose) {
|
||||
dispose();
|
||||
dispose = null;
|
||||
}
|
||||
});
|
||||
|
||||
play();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 612 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
@@ -13,7 +13,7 @@
|
||||
<script>document.documentElement.classList.add('wesp-page-enter-pending');</script>
|
||||
<script src="/static/js/wesp-page-enter.js?v=2"></script>
|
||||
<script src="/static/js/wesp-nav-logo-shimmer.js?v=2"></script>
|
||||
<script src="/static/js/wesp-zootech-nav.js?v=5"></script>
|
||||
<script src="/static/js/wesp-zootech-nav.js?v=6"></script>
|
||||
|
||||
<style>
|
||||
html { background: #f5f7fa; }
|
||||
@@ -24,12 +24,12 @@
|
||||
<link href="/static/css/wesp-zootech-shell.css?v=3" rel="stylesheet">
|
||||
<link href="/static/css/wesp-feed-quality-settings.css?v=3" rel="stylesheet">
|
||||
<link href="/static/css/inter-fonts.css" rel="stylesheet">
|
||||
<link href="/static/css/wesp-zootech-layout.css?v=16" rel="stylesheet"> <link href="/static/css/wesp-zootech-components.css?v=8" rel="stylesheet">
|
||||
<link href="/static/css/wesp-zootech-layout.css?v=17" rel="stylesheet"> <link href="/static/css/wesp-zootech-components.css?v=8" rel="stylesheet">
|
||||
<link href="/static/css/wesp-zootech-shell-modals.css?v=4" rel="stylesheet">
|
||||
<link href="/static/css/wesp-zootech-k-hub.css?v=28" rel="stylesheet">
|
||||
<link href="/static/css/wesp-recipes-skeleton.css" rel="stylesheet">
|
||||
<link href="/static/css/wesp-logo-loader.css" rel="stylesheet">
|
||||
<link href="/static/css/wesp-recipes-mobile.css?v=15" rel="stylesheet">
|
||||
<link href="/static/css/wesp-recipes-mobile.css?v=17" rel="stylesheet">
|
||||
<link href="/static/css/wesp-recipes-editor.css?v=12" rel="stylesheet">
|
||||
<link href="/static/css/font-awesome.min.css" rel="stylesheet">
|
||||
<script src="/static/js/wesp-zootech-notification-messages.js?v=1"></script>
|
||||
@@ -2451,6 +2451,7 @@
|
||||
if (show) {
|
||||
recipeEdit.style.display = 'block';
|
||||
if (isMobile) {
|
||||
window.WespZootechNavCloseMobileMenu?.();
|
||||
document.body.classList.add('recipe-edit-mobile-open');
|
||||
recipeEdit.classList.add('recipe-edit--mobile-active');
|
||||
portalRecipeEditForMobile(recipeEdit);
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<link href="/static/css/font-awesome.min.css" rel="stylesheet">
|
||||
<!-- Добавляем библиотеку SheetJS -->
|
||||
<script src="/static/js/xlsx.full.min.js"></script>
|
||||
<link href="/static/css/wesp-reports-page.css?v=17" rel="stylesheet">
|
||||
<link href="/static/css/wesp-reports-page.css?v=20" rel="stylesheet">
|
||||
<link href="/static/css/wesp-feed-quality-settings.css?v=3" rel="stylesheet">
|
||||
<link href="/static/css/wesp-zootech-pages-polish.css?v=9" rel="stylesheet">
|
||||
<style>
|
||||
@@ -365,12 +365,12 @@
|
||||
<script src="/static/js/pages/daily-plan-component-norms-modal.js?v=23"></script>
|
||||
<script src="/static/js/wesp-zootech-notification-center.js?v=13"></script>
|
||||
<script src="/static/js/pages/analytics-copy.js?v=1"></script>
|
||||
<script src="/static/js/pages/reports-filters.js?v=1"></script>
|
||||
<script src="/static/js/pages/reports-date-range.js?v=4"></script>
|
||||
<script src="/static/js/pages/reports-filters.js?v=2"></script>
|
||||
<script src="/static/js/pages/reports-date-range.js?v=7"></script>
|
||||
<script src="/static/js/pages/analytics-summary.js?v=4"></script>
|
||||
<script src="/static/js/pages/analytics-plan-fact.js?v=5"></script>
|
||||
<script src="/static/js/pages/reports-filter-dropdown.js?v=1"></script>
|
||||
<script src="/static/js/pages/reports-export.js?v=1"></script>
|
||||
<script src="/static/js/pages/reports-filter-dropdown.js?v=3"></script>
|
||||
<script src="/static/js/pages/reports-export.js?v=3"></script>
|
||||
<script src="/static/js/pages/feed-alerts.js?v=11"></script>
|
||||
<script>
|
||||
window.reportsDispenserIdByName = window.reportsDispenserIdByName || {};
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Выбор роли</title>
|
||||
<link href="/static/css/inter-fonts.css" rel="stylesheet">
|
||||
<script src="/static/js/wesp-theme-boot.js"></script>
|
||||
<link href="/static/css/wesp-zootech-layout.css?v=16" rel="stylesheet"> <link href="/static/css/wesp-zootech-components.css" rel="stylesheet">
|
||||
<style>
|
||||
body.role-selection-body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: var(--zootech-font);
|
||||
background: var(--bg-page);
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.role-selection-header {
|
||||
background: var(--gradient-blue);
|
||||
color: #fff;
|
||||
padding: 1.25rem 1rem 1.5rem;
|
||||
text-align: center;
|
||||
box-shadow: var(--box-shadow-hover);
|
||||
}
|
||||
|
||||
.role-selection-header .logo {
|
||||
height: 72px;
|
||||
width: auto;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.role-selection-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: clamp(1.25rem, 4vw, 2rem);
|
||||
}
|
||||
|
||||
.role-selection-grid {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.role-card {
|
||||
flex: 1;
|
||||
background: var(--bg-white);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1.75rem 1.5rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: box-shadow var(--transition), border-color var(--transition);
|
||||
box-shadow: var(--shadow-soft);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.role-card:hover {
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
|
||||
.role-icon {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin: 0 auto 1.25rem;
|
||||
background: var(--bg-light);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.role-title {
|
||||
font-size: 1.35rem;
|
||||
color: var(--primary-color);
|
||||
margin: 0 0 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.role-description {
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 1.25rem;
|
||||
line-height: 1.55;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.role-button {
|
||||
background: var(--gradient-blue);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.85rem 1.25rem;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: filter var(--transition), transform var(--transition);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.role-button:hover {
|
||||
filter: brightness(1.05);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.role-selection-header .logo {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<link href="/static/css/wesp-zootech-theme-organic.css?v=6" rel="stylesheet">
|
||||
</head>
|
||||
<body class="role-selection-body">
|
||||
<header class="role-selection-header">
|
||||
<a href="/recipes" class="nav-logo-link" aria-label="Рецепты">
|
||||
<img src="/static/logo.png" alt="Комптон" class="logo">
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<main class="role-selection-main">
|
||||
<div class="role-selection-grid">
|
||||
<div class="role-card" onclick="selectRole('zootechnician')">
|
||||
<div class="role-icon" aria-hidden="true">📋</div>
|
||||
<h2 class="role-title">Зоотехник</h2>
|
||||
<p class="role-description">Управление рецептами и контроль кормления</p>
|
||||
<button type="button" class="role-button">Войти как зоотехник</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function selectRole(role) {
|
||||
if (role === 'zootechnician') {
|
||||
window.location.href = '/recipes';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#0f1114">
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var saved = localStorage.getItem("theme");
|
||||
var theme = saved === "light" || saved === "dark" ? saved : "dark";
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<title>Комптон — первоначальная настройка</title>
|
||||
<link rel="stylesheet" href="/static/css/wesp-logo-loader.css">
|
||||
<link rel="stylesheet" href="/static/css/wesp-setup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="wespSetupRoot" class="wesp-setup"></div>
|
||||
<script src="/static/js/kiosk-calibration.js"></script>
|
||||
<script src="/static/js/wesp-user-messages.js?v=1"></script>
|
||||
<script type="module" src="/static/js/pages/setup-wizard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,219 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" translate="no" class="notranslate" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="google" content="notranslate">
|
||||
<meta name="googlebot" content="notranslate">
|
||||
<meta http-equiv="Content-Language" content="ru">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
|
||||
<link rel="icon" href="/static/favicon-32.png" type="image/png" sizes="32x32">
|
||||
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
|
||||
<title>WESP — загрузка</title>
|
||||
<link rel="stylesheet" href="/static/css/wesp-logo-loader.css">
|
||||
<link rel="stylesheet" href="/static/css/wesp-setup.css">
|
||||
<link rel="stylesheet" href="/static/css/wesp-startup.css">
|
||||
<link rel="modulepreload" href="/static/js/wesp-logo-loader.js?v=startup11s">
|
||||
</head>
|
||||
<body class="wesp-startup-screen">
|
||||
<div class="wesp-startup__stage">
|
||||
<div class="wesp-setup__intro" id="startupIntro" aria-hidden="false"></div>
|
||||
<p class="wesp-startup__quip" id="startupQuip" aria-live="polite"></p>
|
||||
</div>
|
||||
<noscript>
|
||||
<p class="wesp-startup__quip wesp-startup__quip--error">
|
||||
Ошибка: нужен JavaScript для экрана загрузки.
|
||||
</p>
|
||||
</noscript>
|
||||
<script type="module">
|
||||
import { mountWespLogoLoader } from "/static/js/wesp-logo-loader.js?v=startup11s";
|
||||
|
||||
const quipEl = document.getElementById("startupQuip");
|
||||
const introEl = document.getElementById("startupIntro");
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
let defaultReturn = "/login";
|
||||
const rawNext = params.get("next");
|
||||
let returnTo = defaultReturn;
|
||||
if (rawNext && String(rawNext).trim().startsWith("/")) {
|
||||
returnTo = String(rawNext).trim();
|
||||
} else {
|
||||
const stored = sessionStorage.getItem("wespStartupReturn");
|
||||
if (stored && String(stored).trim().startsWith("/") && stored !== "/") {
|
||||
returnTo = String(stored).trim();
|
||||
}
|
||||
}
|
||||
sessionStorage.setItem("wespStartupReturn", returnTo);
|
||||
const theme = (params.get("theme") || "dark").trim().toLowerCase();
|
||||
if (theme === "light" || theme === "dark" || theme === "organic") {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
}
|
||||
|
||||
const QUIPS = [
|
||||
"Сортируем гранулы по размеру мысли…",
|
||||
"Калибруем весы — не дышите на тару",
|
||||
"Синхронизируем рецепты с ближайшей галактикой",
|
||||
"Ищем соседей по сети — здравствуйте, VLAN",
|
||||
"Считаем компоненты рациона (почти честно)",
|
||||
"Проверяем: server и client — не один Pi",
|
||||
"Чайник почти вскипел, осталось чуть-чуть",
|
||||
"Подтягиваем склад: где мука, там и отчёты",
|
||||
"Собираем пиксели логотипа в боевой порядок",
|
||||
"Готовим весы к первому «ну почти ноль»",
|
||||
"Дозатор №3 просит ещё пять грамм терпения",
|
||||
"Зоотехник ещё не проснулся — мы уже работаем",
|
||||
"Перемалываем корм в биты и обратно",
|
||||
"Сверяем тару с совестью",
|
||||
"Логотип синеет — это не баг, это бренд",
|
||||
"Кормление через пять… четыре… ну почти",
|
||||
"mDNS: «есть кто живой?» — ждём ответа",
|
||||
"Весы молчат — значит, всё стабильно",
|
||||
"Рецепт «на глаз» конвертируем в граммы",
|
||||
"Проверяем, что ноль — действительно ноль",
|
||||
"Комптон заряжает пиксели…",
|
||||
];
|
||||
|
||||
const READY_QUIP = "Готово — открываем двери";
|
||||
const QUIP_ROTATE_MS = 1500;
|
||||
const STARTUP_MIN_MS = 10000;
|
||||
/** Скорость волн/shimmer — как раньше (не растягивать на 10 с). */
|
||||
const STARTUP_LOGO_ANIM_MS = 6000;
|
||||
const READY_HOLD_MS = 700;
|
||||
|
||||
function shuffleQuips(list) {
|
||||
const deck = list.slice();
|
||||
for (let i = deck.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[deck[i], deck[j]] = [deck[j], deck[i]];
|
||||
}
|
||||
return deck;
|
||||
}
|
||||
|
||||
let quipDeck = shuffleQuips(QUIPS);
|
||||
let deckPos = 0;
|
||||
let quipFadeSeq = 0;
|
||||
let quipRotationEnabled = true;
|
||||
let pollTimer = null;
|
||||
let quipTimer = null;
|
||||
let serverReady = false;
|
||||
let finishStarted = false;
|
||||
const minLeaveAt = performance.now() + STARTUP_MIN_MS;
|
||||
|
||||
function pickQuip() {
|
||||
if (deckPos >= quipDeck.length) {
|
||||
quipDeck = shuffleQuips(QUIPS);
|
||||
deckPos = 0;
|
||||
}
|
||||
return quipDeck[deckPos++];
|
||||
}
|
||||
|
||||
function setQuip(text, isError = false) {
|
||||
if (!quipEl) return;
|
||||
const next = String(text || "").trim();
|
||||
if (!next) return;
|
||||
|
||||
const seq = ++quipFadeSeq;
|
||||
quipEl.classList.add("wesp-startup__quip--fade");
|
||||
quipEl.classList.toggle("wesp-startup__quip--error", Boolean(isError));
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (seq !== quipFadeSeq) return;
|
||||
quipEl.textContent = next;
|
||||
quipEl.classList.remove("wesp-startup__quip--fade");
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function stopQuipRotation() {
|
||||
quipRotationEnabled = false;
|
||||
if (quipTimer) {
|
||||
clearInterval(quipTimer);
|
||||
quipTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function rotateQuip() {
|
||||
if (!quipRotationEnabled || document.hidden || finishStarted) return;
|
||||
setQuip(pickQuip());
|
||||
}
|
||||
|
||||
function tryFinish() {
|
||||
if (finishStarted) return;
|
||||
if (!serverReady) return;
|
||||
if (performance.now() < minLeaveAt) return;
|
||||
|
||||
finishStarted = true;
|
||||
stopQuipRotation();
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
|
||||
setQuip(READY_QUIP);
|
||||
const target = returnTo.startsWith("/") ? returnTo : defaultReturn;
|
||||
window.setTimeout(() => window.location.replace(target), READY_HOLD_MS);
|
||||
}
|
||||
|
||||
async function pollReady() {
|
||||
if (finishStarted) return;
|
||||
try {
|
||||
const r = await fetch("/api/startup/status", { cache: "no-store" });
|
||||
const d = await r.json().catch(() => ({}));
|
||||
|
||||
if (d.default_next && String(d.default_next).trim().startsWith("/")) {
|
||||
defaultReturn = String(d.default_next).trim();
|
||||
if (!rawNext) {
|
||||
returnTo = defaultReturn;
|
||||
sessionStorage.setItem("wespStartupReturn", returnTo);
|
||||
}
|
||||
}
|
||||
|
||||
if (d.ready) {
|
||||
serverReady = true;
|
||||
tryFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (d.phase === "error" || d.error) {
|
||||
finishStarted = true;
|
||||
stopQuipRotation();
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
setQuip(d.error || d.message || "Ошибка инициализации", true);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
if (quipRotationEnabled && !quipEl.textContent) {
|
||||
setQuip("Ожидание сервера…");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await mountWespLogoLoader(introEl, {
|
||||
loop: false,
|
||||
durationMs: STARTUP_LOGO_ANIM_MS,
|
||||
embedded: true,
|
||||
transparentBackground: true,
|
||||
knockoutBackground: true,
|
||||
theme: document.documentElement.getAttribute("data-theme") || theme,
|
||||
hideChrome: false,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("startup logo loader failed", err);
|
||||
finishStarted = true;
|
||||
stopQuipRotation();
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
setQuip(
|
||||
"Не удалось загрузить анимацию. Проверьте /static/js/wesp-logo-loader.js и логи сервера.",
|
||||
true,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (quipEl) quipEl.classList.remove("wesp-startup__quip--error");
|
||||
setQuip(pickQuip());
|
||||
quipTimer = window.setInterval(rotateQuip, QUIP_ROTATE_MS);
|
||||
pollReady();
|
||||
pollTimer = window.setInterval(pollReady, 800);
|
||||
window.setTimeout(tryFinish, STARTUP_MIN_MS);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Неавторизованное устройство</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background:#111; color:#fff; display:flex; align-items:center; justify-content:center; min-height:100vh; margin:0; }
|
||||
.card { text-align:center; background:#1f1f1f; padding:28px; border-radius:12px; box-shadow:0 0 20px rgba(0,0,0,0.4); max-width:560px; width:92%; }
|
||||
h1 { margin-bottom:16px; font-size:26px; }
|
||||
p { margin-bottom:18px; line-height:1.5; }
|
||||
.btn { display:inline-block; margin-top:8px; padding:10px 16px; border-radius:8px; border:1px solid #4ea8ff; color:#fff; background:#1b66a8; cursor:pointer; font-size:15px; }
|
||||
.btn:disabled { opacity:0.6; cursor:not-allowed; }
|
||||
.hint { font-size:14px; color:#d1d1d1; margin-top:16px; text-align:left; line-height:1.5; }
|
||||
.hint ol { margin:10px 0 0 0; padding-left:22px; }
|
||||
.hint li { margin-bottom:8px; }
|
||||
.status { margin-top:14px; min-height:22px; font-size:14px; color:#d6e8ff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Неавторизованное устройство</h1>
|
||||
<p>Этот планшет ещё не настроен для работы с весами.</p>
|
||||
<div class="hint">
|
||||
<strong>Что делать:</strong>
|
||||
<ol>
|
||||
<li>На весах нажмите "Привязать терминал".</li>
|
||||
<li>Отсканируйте QR-код на экране.</li>
|
||||
<li>Вставте полученную ссылку в поле <strong>Fully Kiosk Browser</strong> → <strong>Settings</strong> → <strong>Web Content Settings</strong> → <strong>Start URL</strong>.</li>
|
||||
<li>Перезапустите Fully Kiosk Browser.</li>
|
||||
</ol>
|
||||
<p style="margin:12px 0 0 0;font-size:13px;color:#a8a8a8">
|
||||
Ограничение доступа сделано для безопасности фермы. Вы можете получить доступ только с помощью QR-кода.
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn" id="checkBtn" type="button">Проверить снова</button>
|
||||
<div class="status" id="statusText"></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var checkBtn = document.getElementById("checkBtn");
|
||||
var statusText = document.getElementById("statusText");
|
||||
var nextParam = new URLSearchParams(window.location.search).get("next") || "/scales";
|
||||
|
||||
function setStatus(text, isError) {
|
||||
statusText.textContent = text;
|
||||
statusText.style.color = isError ? "#ff9f9f" : "#d6e8ff";
|
||||
}
|
||||
|
||||
async function checkPairedStatus() {
|
||||
checkBtn.disabled = true;
|
||||
setStatus("Проверяем…");
|
||||
try {
|
||||
var response = await fetch("/api/kiosk/status");
|
||||
var payload = await response.json().catch(function () { return {}; });
|
||||
if (!response.ok) {
|
||||
setStatus("Не удалось проверить. Попробуйте ещё раз.", true);
|
||||
return false;
|
||||
}
|
||||
if (!payload.paired && payload.enforce_paired_only !== false) {
|
||||
setStatus("Терминал пока не настроен. Дождитесь администратора.", true);
|
||||
return false;
|
||||
}
|
||||
setStatus("Готово. Открываем экран весов…");
|
||||
window.setTimeout(function () {
|
||||
window.location.href = nextParam;
|
||||
}, 600);
|
||||
return true;
|
||||
} catch (_e) {
|
||||
setStatus("Нет связи с сервером. Проверьте Wi‑Fi фермы.", true);
|
||||
return false;
|
||||
} finally {
|
||||
checkBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
checkBtn.addEventListener("click", checkPairedStatus);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||