From c7fecfba0bf897ab54b0f95d85d036c7d1315a20 Mon Sep 17 00:00:00 2001 From: byrsapty Date: Thu, 27 Aug 2026 22:18:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B3=D0=BE=D1=80=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8F=20=D0=BC=D0=B5=D0=BD=D1=8E,=20=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=B7=20=D1=84=D0=B0=D0=B2=D1=96=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D0=BE=D0=BC,=20=D0=B2=D0=B8=D1=80=D1=96=D0=B2=D0=BD=D1=8E?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=BD=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=96=D0=B2?= =?UTF-8?q?=20=D1=96=D0=B7=20=D0=BF=D1=96=D0=B4=D0=BA=D0=B0=D0=B7=D0=BA?= =?UTF-8?q?=D0=BE=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit МЕНЮ. Шість груп, разом 1246 px при вікні 720 — довше за екран. Тепер згортаються, стан живе в localStorage як перелік ЗГОРНУТИХ (не розгорнутих: інакше кожна нова група приїхала б до наявних користувачів схованою). Ключ групи окремо від заголовка — підписи тут уже переписували, і якби ключем був заголовок, кожне переписування слова тихо скидало б налаштування всім. Група з активною сторінкою розгортається сама, але рівно одна — вибір для решти не чіпається. Правка стану на ЗМІНІ шляху, а не на кожному рендері, тож згорнути активну групу теж можна. Дві наявні поломки мобільного вигляду, знайдені дорогою: рейка вмикалась класами md:, а розгалуження заголовок/риска було на JS і про md: не знало — людина згортала панель на моніторі, і в шухляді на телефоні ЗНИКАЛИ ВСІ назви розділів. ЛОГО. Пульс на лінії зв'язку: вузол → сплеск → провал → норма → вузол. Два вузли по краях — ключова деталь: без них це іконка «activity» з будь-якого безкоштовного набору, з ними кардіограма стає лінією між двома точками мережі. SVG кодом, не растр: слідує темі через currentColor, важить сотні байтів. Три РІЗНІ малюнки, а не один масштабований: повне лого, знак, і спрощений для 16 px усередині favicon.ico (зібраний власним кодувальником на zlib, без сторонніх інструментів). Пульсація вузла — опційна й типово вимкнена: у шапці вона конкурувала б зі справжнім показником зв'язку, який стоїть поруч. ПОЛЯ З ПІДКАЗКОЮ. Field малює підказку під контролем, тож у ряду з вирівнюванням по низу колонка з підказкою вища — її підпис і поле спливають угору, а гола кнопка з'їжджає вниз. Видно було в словнику трапів; та сама вада знайшлась у редакторі фільтра портів. Ряди вирівняно по верху, а кнопки без підпису загорнуто у FieldGap — порожній підпис такої самої висоти. Відступ у пікселях полагодив би один ряд і роз'їхався б від зміни розміру шрифту. Перевірено очима в живому Chrome: іконки віддаються (4/4, правильні типи й розміри), лого 64×64 на сторінці входу, праве коло справді sky-400 і пульсує лише там. --- web/index.html | 58 +++++++++- web/preview-nav.html | 12 -- web/public/apple-touch-icon.png | Bin 0 -> 3997 bytes web/public/favicon-32.png | Bin 0 -> 1217 bytes web/public/favicon.ico | Bin 0 -> 3537 bytes web/public/favicon.svg | 26 +++++ web/src/components/AppShell.tsx | 19 +++- web/src/components/InterfaceFilterEditor.tsx | 12 +- web/src/components/LoginPage.tsx | 27 ++++- web/src/components/Logo.tsx | 114 +++++++++++++++++++ web/src/components/ui.tsx | 23 ++++ web/src/index.css | 18 +++ web/src/pages/TrapsPage.tsx | 11 +- web/src/preview-nav.tsx | 43 ------- 14 files changed, 286 insertions(+), 77 deletions(-) delete mode 100644 web/preview-nav.html create mode 100644 web/public/apple-touch-icon.png create mode 100644 web/public/favicon-32.png create mode 100644 web/public/favicon.ico create mode 100644 web/public/favicon.svg create mode 100644 web/src/components/Logo.tsx delete mode 100644 web/src/preview-nav.tsx diff --git a/web/index.html b/web/index.html index 34a557d..4187137 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,63 @@ - NetPulse + + NetPulse — моніторинг мережі + + + + + + + + + + + + + + + + + + + + +
diff --git a/web/preview-nav.html b/web/preview-nav.html deleted file mode 100644 index e5a50d0..0000000 --- a/web/preview-nav.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - NetPulse — огляд меню - - -
- - - diff --git a/web/public/apple-touch-icon.png b/web/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bea8e0fe1dbcbac1a3eb3ace95ced989b5cb6688 GIT binary patch literal 3997 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 + + + + + + diff --git a/web/src/components/AppShell.tsx b/web/src/components/AppShell.tsx index 61aba1b..b75015e 100644 --- a/web/src/components/AppShell.tsx +++ b/web/src/components/AppShell.tsx @@ -5,6 +5,7 @@ import { session } from '../api/session' import { live } from '../api/ws' import { useAlerts } from '../hooks/useAlerts' import type { Permission } from '../types' +import { LogoMark } from './Logo' import { plural, useWideScreen } from './ui' interface NavItem { @@ -399,7 +400,10 @@ export function AppShell() { ☰ - NetPulse + + + NetPulse +
{/* Замерзлий інтерфейс виглядає точно так само, як справний. @@ -516,10 +520,10 @@ export function AppShell() { onClick={() => toggleGroup(g.id)} aria-expanded={open} aria-controls={panelID} - // py-3 на телефоні, а не таке, як на моніторі: там у - // заголовок цілять пальцем, і 26 px висоти — це - // промах через раз. Поруч пункти по 40 px, тож - // однакова висота ще й вирівнює список. + // На телефоні заголовок вищий, ніж на моніторі: там у + // нього цілять пальцем, а спільні 31 px — це промах + // через раз. Поруч пункти по 40 px, тож py-3 ще й + // вирівнює список. // // Фокус — outline, а не ring: ring малюється тінню, // а тіні зникають у режимі високої контрастності @@ -567,7 +571,10 @@ export function AppShell() { ) : ( {g.items.length} diff --git a/web/src/components/InterfaceFilterEditor.tsx b/web/src/components/InterfaceFilterEditor.tsx index fd62329..3bca41e 100644 --- a/web/src/components/InterfaceFilterEditor.tsx +++ b/web/src/components/InterfaceFilterEditor.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react' import { api } from '../api/client' -import { Button, ErrorNote, Field, formatBps, inputClass, plural } from './ui' +import { Button, ErrorNote, Field, FieldGap, formatBps, inputClass, plural } from './ui' import type { IfFilterField, IfFilterOp, @@ -344,7 +344,7 @@ export function InterfaceFilterEditor({ {/* ---- Перевірка на справжньому хості ---- */}
-
+
- + + +
{err} diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index 24d890d..6c1ec20 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { ApiError, api } from '../api/client' import { meFromLogin, session } from '../api/session' import type { TenantMembership } from '../types' +import { LogoMark } from './Logo' /** * Сторінка входу. @@ -57,15 +58,29 @@ export function LoginPage({ onDone }: { onDone: () => void }) { } return ( -
+
-
-
NetPulse
-
моніторинг мережі
+ {/* Вхід — єдиний екран, який людина бачить до того, як щось + запрацює. Тому знак тут великий і стоїть над карткою, а не + рядком у кутку: за ці кілька секунд продукт або виглядає + зробленим, або не виглядає ніяк. + + text-slate-100 на самому знаку обов'язково: штрих малюється + через currentColor, а на цій сторінці колір тексту ніде не + заданий — знак успадкував би типовий чорний і зник би на + темному тлі. */} +
+ {/* Квадрат тут більший, ніж здається потрібним: малюнок займає + лише середню смугу поля зору (пульс не сягає ані верху, ані + низу сітки 32×32), тож знак «на 56 px» насправді має висоту + 37. Розмір підібрано за шириною слова під ним. */} + +
NetPulse
+
моніторинг мережі та конфігурацій
{tenants ? ( -
+

Оберіть організацію

    {tenants.map((t) => ( @@ -85,7 +100,7 @@ export function LoginPage({ onDone }: { onDone: () => void }) {
) : (
{ e.preventDefault() void submit() diff --git a/web/src/components/Logo.tsx b/web/src/components/Logo.tsx new file mode 100644 index 0000000..cccf005 --- /dev/null +++ b/web/src/components/Logo.tsx @@ -0,0 +1,114 @@ +/** + * Лого NetPulse. + * + * Намальоване кодом, а не покладене растром, і причина не в вазі файла + * (хоч знак і важить менше кілобайта). Растр має вшиті кольори: на + * темному тлі світлий PNG сидить світлою плямою, ніби наліпка, а + * підігнати його під тему можна лише другим файлом. Тут штрих іде + * через currentColor — знак бере колір тексту від батька й тому + * правильний скрізь, куди його поставлять, без жодної домовленості. + * + * Знак і повне лого — різні експорти навмисно. У згорнутому меню й у + * фавіконі місця рівно на квадрат; тягнути туди слово «NetPulse» + * означало б показувати його завширшки в чотири пікселі на літеру. + * + * Той самий малюнок повторено у web/public/favicon.svg. Дублювання + * свідоме: фавікон вантажиться браузером окремим файлом ще до того, як + * виконається бодай рядок JS, тож поділитися з ним компонентом нема як. + * Якщо правитимете геометрію — правте в обох місцях. + */ + +/** + * Пульс на лінії зв'язку, сітка 32×32. + * + * Читається зліва направо: вузол, спокійна ділянка, різкий сплеск із + * провалом, вихід на норму, другий вузол. Це не абстракція — це те, що + * NetPulse показує на графіку відгуку, коли пристрій моргнув. + * + * Координати кратні 0.5 навмисно: на 16 px чверть одиниці лягає між + * пікселями, і замість лінії виходить сіра смуга. + */ +const TRACE = 'M3.5 18.5h7l3.5-11.5 4.5 18 3-6.5h7' + +interface MarkProps { + /** Розмір задається класами висоти й ширини: у різних місцях він різний. */ + className?: string + /** + * Підпис для читача з екрана. Потрібен лише там, де знак стоїть сам: + * поруч зі словом «NetPulse» він озвучив би назву двічі. + */ + label?: string + /** + * Повільне блимання живого вузла. Вмикаємо тільки на вході — там знак + * великий і людина на нього дивиться. У шапці те саме блимання за + * робочий день перетворюється на подразник. + */ + pulse?: boolean +} + +export function LogoMark({ className = 'h-7 w-7', label, pulse = false }: MarkProps) { + return ( + + + + {/* Другий вузол — блакитний: він означає «звідси щойно прийшла + відповідь». Той самий колір, яким у списках позначено живий + стан, тож знак і інтерфейс говорять одне й те саме. */} + + + ) +} + +const SIZES = { + sm: { mark: 'h-5 w-5', text: 'text-sm', gap: 'gap-2' }, + md: { mark: 'h-7 w-7', text: 'text-lg', gap: 'gap-2.5' }, + lg: { mark: 'h-12 w-12', text: 'text-3xl', gap: 'gap-3' }, +} as const + +/** + * Повне лого: знак плюс назва. + * + * Назва — справжній текст, а не обведений контур. Контур виглядав би + * однаково скрізь, але його не прочитає ні пошук, ні читач з екрана, і + * він не успадкує шрифт інтерфейсу — тобто на сторінці, зробленій + * системним шрифтом, стояв би чужий. + */ +export function Logo({ + size = 'md', + className = '', + pulse = false, +}: { + size?: keyof typeof SIZES + className?: string + pulse?: boolean +}) { + const s = SIZES[size] + return ( + + + {/* tracking-tight — та сама щільність, що й у решті заголовків + проєкту: лого, набране ширше за сусідній текст, читається як + картинка, вставлена з іншого макета. */} + NetPulse + + ) +} diff --git a/web/src/components/ui.tsx b/web/src/components/ui.tsx index 981fcbb..f8e0ac3 100644 --- a/web/src/components/ui.tsx +++ b/web/src/components/ui.tsx @@ -136,6 +136,29 @@ export function Field({ ) } +// FieldGap — порожній підпис заввишки зі справжній. +// +// Потрібен там, де в одному ряду сітки стоять поля й гола кнопка. Поле +// складається з підпису та контролю, кнопка — лише з себе; при +// вирівнюванні по ВЕРХУ кнопка з'їжджає на висоту підпису вгору, а при +// вирівнюванні по НИЗУ вгору з'їжджає вже поле з підказкою, бо підказка +// малюється під контролем і робить колонку вищою. +// +// Саме це й побачили в словнику трапів: «Пояснення» стояло вище за +// сусідів, а кнопка — нижче. Магічний відступ у пікселях полагодив би +// один ряд і роз'їхався б від зміни розміру шрифту; порожній підпис +// займає рівно ту саму коробку, що й справжній, тому не роз'їдеться. +export function FieldGap({ children }: { children: ReactNode }) { + return ( +
+ +   + + {children} +
+ ) +} + export const inputClass = 'w-full rounded border border-slate-700 bg-slate-800 px-3 py-2 text-sm text-slate-100 ' + 'outline-none placeholder:text-slate-600 focus:border-sky-600' diff --git a/web/src/index.css b/web/src/index.css index 2c85b5c..8d8f559 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -27,6 +27,24 @@ body { color: #334155 !important; } +/* --------------------------------------------------------------------- + Сторінка входу + --------------------------------------------------------------------- + Та сама радіальна підсвітка, що й на полотні мапи, тільки слабша й + зміщена вгору — під знак і картку. Рівне чорне тло технічно не гірше, + але вхід — єдиний екран, який бачать до того, як продукт хоч щось + зробив, і плаский прямокутник на ньому читається як незавершена + сторінка. Градієнт тут не прикраса, а єдиний доступний спосіб сказати + «це закінчена річ» до першого корисного пікселя. + + Клас, а не правило на body: після входу тло малює вже AppShell, і + градієнт, лишений глобально, проступав би крізь усі сторінки. +*/ +.netpulse-auth { + background: + radial-gradient(900px 520px at 50% 28%, #16233a 0%, #0a1120 55%, #020617 100%); +} + /* --------------------------------------------------------------------- Кнопки — це кнопки, а не текст ------------------------------------------------------------------ */ diff --git a/web/src/pages/TrapsPage.tsx b/web/src/pages/TrapsPage.tsx index 8339107..c5c6961 100644 --- a/web/src/pages/TrapsPage.tsx +++ b/web/src/pages/TrapsPage.tsx @@ -7,6 +7,7 @@ import { Card, ErrorNote, Field, + FieldGap, PageBody, PageHeader, Spinner, @@ -629,7 +630,7 @@ function Dictionary({

{canEdit && ( -
+
setDesc(e.target.value)} /> - + + +
)} {err} diff --git a/web/src/preview-nav.tsx b/web/src/preview-nav.tsx deleted file mode 100644 index 8568b06..0000000 --- a/web/src/preview-nav.tsx +++ /dev/null @@ -1,43 +0,0 @@ -// ТИМЧАСОВИЙ стенд для огляду меню очима. Видаляється після перевірки. -import { createRoot } from 'react-dom/client' -import { MemoryRouter, Route, Routes } from 'react-router-dom' -import { AppShell } from './components/AppShell' -import { session } from './api/session' -import type { Permission } from './types' -import './index.css' - -const counts = { firing: 3, acknowledged: 0, suppressed: 0, disaster: 1, high: 2 } - -window.fetch = (async (url: RequestInfo | URL) => { - const u = String(url) - const body = u.includes('/alerts') ? { alerts: [], counts } : {} - return { - status: 200, - ok: true, - statusText: '', - text: async () => JSON.stringify(body), - } as unknown as Response -}) as typeof window.fetch - -class Silent { - close() {} - send() {} -} -;(window as unknown as { WebSocket: unknown }).WebSocket = Silent - -const perms = (new URLSearchParams(location.search).get('perms') ?? '*').split( - ',', -) as Permission[] -const path = new URLSearchParams(location.search).get('path') ?? '/devices' - -session.set('tok', { userID: 'u-1', username: 'admin', tenantID: 't-1', permissions: perms }) - -createRoot(document.getElementById('root')!).render( - - - }> - сторінка
} /> - - - , -)