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 0000000..bea8e0f Binary files /dev/null and b/web/public/apple-touch-icon.png differ diff --git a/web/public/favicon-32.png b/web/public/favicon-32.png new file mode 100644 index 0000000..5312d65 Binary files /dev/null and b/web/public/favicon-32.png differ diff --git a/web/public/favicon.ico b/web/public/favicon.ico new file mode 100644 index 0000000..6a86aaa Binary files /dev/null and b/web/public/favicon.ico differ diff --git a/web/public/favicon.svg b/web/public/favicon.svg new file mode 100644 index 0000000..19a790b --- /dev/null +++ b/web/public/favicon.svg @@ -0,0 +1,26 @@ + + + + + + + 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( - - - }> - сторінка
} /> - - - , -)