Redesign summary panel and compact window layout
This commit is contained in:
@@ -13,9 +13,10 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "ProxyWarden",
|
"title": "ProxyWarden",
|
||||||
"width": 1120,
|
"width": 820,
|
||||||
"height": 760,
|
"height": 760,
|
||||||
"minWidth": 760,
|
"minWidth": 820,
|
||||||
|
"maxWidth": 820,
|
||||||
"minHeight": 560,
|
"minHeight": 560,
|
||||||
"resizable": true
|
"resizable": true
|
||||||
}
|
}
|
||||||
|
|||||||
178
src/app/App.tsx
178
src/app/App.tsx
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { Cpu, FileCode2, FolderOpen, Gauge, Link2, Power, ShieldAlert, Trash2, Wand2 } from 'lucide-react';
|
import { Cpu, FileCode2, FolderOpen, Gauge, Link2, ShieldAlert, Trash2, Wand2 } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
applyProfiles,
|
applyProfiles,
|
||||||
fetchSingBoxSubscription,
|
fetchSingBoxSubscription,
|
||||||
@@ -50,6 +50,7 @@ type ServiceVisualState = 'active' | 'settling' | null;
|
|||||||
type PanelId = 'summary' | 'proxifyre' | 'proxy';
|
type PanelId = 'summary' | 'proxifyre' | 'proxy';
|
||||||
type StatusTone = 'ok' | 'warning' | 'error' | 'checking' | 'muted';
|
type StatusTone = 'ok' | 'warning' | 'error' | 'checking' | 'muted';
|
||||||
type TabTransitionDirection = 'left' | 'right';
|
type TabTransitionDirection = 'left' | 'right';
|
||||||
|
type SummaryRouteFlow = 'proxy' | 'direct' | 'idle';
|
||||||
|
|
||||||
interface DraftItem {
|
interface DraftItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -140,7 +141,9 @@ const MAIN_TARGET_ID = 'main-proxy';
|
|||||||
const MAIN_PROFILE_ID = 'main-profile';
|
const MAIN_PROFILE_ID = 'main-profile';
|
||||||
const LOCAL_SINGBOX_TARGET_ID = 'local-singbox';
|
const LOCAL_SINGBOX_TARGET_ID = 'local-singbox';
|
||||||
const LOG_VISIBLE_MS = 6500;
|
const LOG_VISIBLE_MS = 6500;
|
||||||
const PANEL_ORDER: PanelId[] = ['summary', 'proxifyre', 'proxy'];
|
const PANEL_ORDER: PanelId[] = ['proxifyre', 'summary', 'proxy'];
|
||||||
|
const proxyWardenToggleOnImage = new URL('../assets/proxywarden-toggle-on.png', import.meta.url).href;
|
||||||
|
const proxyWardenToggleOffImage = new URL('../assets/proxywarden-toggle-off.png', import.meta.url).href;
|
||||||
|
|
||||||
const fallbackComponents: ComponentStatus[] = [
|
const fallbackComponents: ComponentStatus[] = [
|
||||||
{
|
{
|
||||||
@@ -993,17 +996,15 @@ export function App() {
|
|||||||
|
|
||||||
function renderTabs() {
|
function renderTabs() {
|
||||||
const tabs: Array<{ id: PanelId; label: string }> = [
|
const tabs: Array<{ id: PanelId; label: string }> = [
|
||||||
{ id: 'summary', label: 'Сводка' },
|
|
||||||
{ id: 'proxifyre', label: 'ProxiFyre' },
|
{ id: 'proxifyre', label: 'ProxiFyre' },
|
||||||
|
{ id: 'summary', label: 'ProxyWarden' },
|
||||||
{ id: 'proxy', label: 'VPN / Прокси' },
|
{ id: 'proxy', label: 'VPN / Прокси' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return <Tabs items={tabs} activeId={activePanel} onChange={switchPanel} ariaLabel="Разделы Proxy" />;
|
return <Tabs items={tabs} activeId={activePanel} onChange={switchPanel} ariaLabel="Разделы ProxyWarden" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSummaryPanel() {
|
function renderSummaryPanel() {
|
||||||
const appliedRouteMode = appliedSnapshot?.routeMode ?? routeMode;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="tab-panel summary-panel"
|
className="tab-panel summary-panel"
|
||||||
@@ -1011,39 +1012,11 @@ export function App() {
|
|||||||
id="panel-summary"
|
id="panel-summary"
|
||||||
aria-labelledby="tab-summary"
|
aria-labelledby="tab-summary"
|
||||||
>
|
>
|
||||||
|
<h1 className="summary-brand-title">ProxyWarden</h1>
|
||||||
|
<div className="summary-main">
|
||||||
{renderSummaryStatusControl()}
|
{renderSummaryStatusControl()}
|
||||||
|
{renderRouteChain('vertical')}
|
||||||
<div className="summary-readout">
|
|
||||||
<div className="summary-readout-head">
|
|
||||||
<div>
|
|
||||||
<span>Применено сейчас</span>
|
|
||||||
<strong>{appliedSnapshot ? routeModeLabel(appliedRouteMode) : 'Состояние загружается'}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<dl className="summary-lines">
|
|
||||||
<div>
|
|
||||||
<dt>ProxiFyre</dt>
|
|
||||||
<dd>{proxyfierTitle(proxyfier, isDetectingComponents)}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Маршрут</dt>
|
|
||||||
<dd>{snapshotRouteLine(appliedSnapshot, singBoxStatus)}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>VPN сервер</dt>
|
|
||||||
<dd>{snapshotServerText(appliedSnapshot)}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Приложения</dt>
|
|
||||||
<dd>{snapshotItemsText(appliedSnapshot)}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Конфиг</dt>
|
|
||||||
<dd>{generatedConfigPath || 'Примененный config еще не создан.'}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1230,30 +1203,28 @@ export function App() {
|
|||||||
const installed = Boolean(proxyfier?.installed);
|
const installed = Boolean(proxyfier?.installed);
|
||||||
const running = Boolean(proxyfier?.running);
|
const running = Boolean(proxyfier?.running);
|
||||||
const working = serviceAction === 'start' || serviceAction === 'stop' || serviceAction === 'restart';
|
const working = serviceAction === 'start' || serviceAction === 'stop' || serviceAction === 'restart';
|
||||||
const buttonLabel = !installed ? 'Не установлен' : running ? 'Отключить' : 'Включить';
|
const stateLabel = working || systemSummary.tone === 'checking'
|
||||||
|
? 'Проверяю'
|
||||||
|
: systemSummary.tone === 'ok' ? 'Работает' : 'Не работает';
|
||||||
|
const buttonAriaLabel = !installed
|
||||||
|
? 'ProxiFyre не установлен'
|
||||||
|
: running ? 'Отключить ProxyWarden' : 'Включить ProxyWarden';
|
||||||
|
const imageSrc = running ? proxyWardenToggleOnImage : proxyWardenToggleOffImage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`summary-status-control ${systemSummary.tone} ${running ? 'on' : 'off'}`}>
|
<div className={`summary-status-control ${systemSummary.tone} ${running ? 'on' : 'off'}`}>
|
||||||
{systemSummary.tone === 'checking' ? <BusyRing /> : null}
|
<button
|
||||||
<span className={`summary-status-dot ${systemSummary.tone}`} aria-hidden="true" />
|
|
||||||
<div className="summary-status-copy">
|
|
||||||
<span>Состояние маршрута</span>
|
|
||||||
<strong>{systemSummary.title}</strong>
|
|
||||||
<p>{summaryControlText(appliedSnapshot, proxyfier, isDetectingComponents)}</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
type="button"
|
||||||
variant={!installed ? 'neutral' : running ? 'danger' : 'primary'}
|
className="summary-toggle-button"
|
||||||
size="lg"
|
|
||||||
className="summary-status-button"
|
|
||||||
leftIcon={<Power size={18} strokeWidth={2} />}
|
|
||||||
onClick={() => void setProxiFyreServiceRunning(!running)}
|
onClick={() => void setProxiFyreServiceRunning(!running)}
|
||||||
disabled={!installed || isDetectingComponents || Boolean(serviceAction)}
|
disabled={!installed || isDetectingComponents || Boolean(serviceAction)}
|
||||||
loading={working}
|
aria-label={buttonAriaLabel}
|
||||||
loadingLabel={serviceAction === 'restart' ? 'Перезапускаю' : running ? 'Отключаю' : 'Включаю'}
|
aria-pressed={installed ? running : undefined}
|
||||||
>
|
>
|
||||||
{buttonLabel}
|
{systemSummary.tone === 'checking' || working ? <BusyRing /> : null}
|
||||||
</Button>
|
<img src={imageSrc} alt="" draggable={false} />
|
||||||
|
</button>
|
||||||
|
<strong className={`summary-state-label ${systemSummary.tone}`}>{stateLabel}</strong>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1602,13 +1573,12 @@ export function App() {
|
|||||||
{routeMode === 'external' ? renderExternalProxyControls() : renderSingBoxCard()}
|
{routeMode === 'external' ? renderExternalProxyControls() : renderSingBoxCard()}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{renderRouteChain()}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRouteChain() {
|
function renderRouteChain(variant: 'horizontal' | 'vertical' = 'horizontal') {
|
||||||
const segments = routeChainSegments({
|
const input: RouteChainInput = {
|
||||||
routeMode,
|
routeMode,
|
||||||
proxyInput,
|
proxyInput,
|
||||||
proxyfier,
|
proxyfier,
|
||||||
@@ -1617,23 +1587,41 @@ export function App() {
|
|||||||
selectedServer,
|
selectedServer,
|
||||||
appCount: items.length,
|
appCount: items.length,
|
||||||
isDetectingComponents,
|
isDetectingComponents,
|
||||||
});
|
};
|
||||||
|
const flow = variant === 'vertical' ? summaryRouteFlow(input) : 'proxy';
|
||||||
|
const segments = variant === 'vertical'
|
||||||
|
? summaryRouteChainSegments(input, flow)
|
||||||
|
: routeChainSegments(input);
|
||||||
|
const isVertical = variant === 'vertical';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="route-chain" aria-label="Текущий маршрут">
|
<section className={`route-chain route-chain--${variant} route-chain--${flow}`} aria-label="Текущий маршрут">
|
||||||
|
{isVertical ? (
|
||||||
|
<span className="route-chain-rail" aria-hidden="true">
|
||||||
|
{flow === 'idle' ? null : (
|
||||||
|
<>
|
||||||
|
<span className="route-chain-packet route-chain-packet--one" />
|
||||||
|
<span className="route-chain-packet route-chain-packet--two" />
|
||||||
|
<span className="route-chain-packet route-chain-packet--three" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
{segments.map((segment, index) => (
|
{segments.map((segment, index) => (
|
||||||
<DetailsPopover
|
<DetailsPopover
|
||||||
className={`route-chain-segment ${segment.tone}`}
|
className={`route-chain-segment ${segment.tone}`}
|
||||||
details={segment.details}
|
details={segment.details}
|
||||||
popoverLabel={segment.label}
|
popoverLabel={segment.label}
|
||||||
align={index >= segments.length - 2 ? 'end' : 'start'}
|
align={isVertical || index >= segments.length - 2 ? 'end' : 'start'}
|
||||||
aria-label={`${segment.label}: ${segment.value}. ${segment.details.join('. ')}`}
|
aria-label={`${segment.label}: ${segment.value}. ${segment.details.join('. ')}`}
|
||||||
key={segment.id}
|
key={segment.id}
|
||||||
>
|
>
|
||||||
<span className="route-chain-dot" aria-hidden="true" />
|
<span className="route-chain-dot" aria-hidden="true" />
|
||||||
<span>{segment.label}</span>
|
<span>{segment.label}</span>
|
||||||
<strong>{segment.value}</strong>
|
<strong>{segment.value}</strong>
|
||||||
{index < segments.length - 1 ? <span className="route-chain-arrow" aria-hidden="true">-></span> : null}
|
{index < segments.length - 1 ? (
|
||||||
|
<span className="route-chain-arrow" aria-hidden="true">{isVertical ? '↓' : '->'}</span>
|
||||||
|
) : null}
|
||||||
</DetailsPopover>
|
</DetailsPopover>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
@@ -1656,23 +1644,6 @@ export function App() {
|
|||||||
style={shellStyle}
|
style={shellStyle}
|
||||||
>
|
>
|
||||||
<section className="simple-panel">
|
<section className="simple-panel">
|
||||||
<header className="simple-header">
|
|
||||||
<div>
|
|
||||||
<h1>ProxyWarden</h1>
|
|
||||||
<small>Proxy для приложений</small>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="neutral"
|
|
||||||
size="sm"
|
|
||||||
onClick={refresh}
|
|
||||||
loading={isLoading || isDetectingComponents}
|
|
||||||
loadingLabel={isLoading ? 'Загружаю' : 'Проверяю'}
|
|
||||||
>
|
|
||||||
Обновить
|
|
||||||
</Button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{renderTabs()}
|
{renderTabs()}
|
||||||
<div className={`tab-panel-frame swipe-${tabTransitionDirection}`} key={activePanel}>
|
<div className={`tab-panel-frame swipe-${tabTransitionDirection}`} key={activePanel}>
|
||||||
{renderActivePanel()}
|
{renderActivePanel()}
|
||||||
@@ -2111,6 +2082,57 @@ function connectionCheckView(input: ConnectionCheckInput): ConnectionCheckView {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function summaryRouteFlow(input: RouteChainInput): SummaryRouteFlow {
|
||||||
|
if (input.isDetectingComponents || input.appCount <= 0) return 'idle';
|
||||||
|
if (!input.proxyfier?.running) return 'direct';
|
||||||
|
return 'proxy';
|
||||||
|
}
|
||||||
|
|
||||||
|
function summaryRouteChainSegments(input: RouteChainInput, flow: SummaryRouteFlow): RouteChainSegment[] {
|
||||||
|
if (flow === 'proxy') return routeChainSegments(input);
|
||||||
|
|
||||||
|
const appSegment: RouteChainSegment = {
|
||||||
|
id: 'apps',
|
||||||
|
label: 'Приложения',
|
||||||
|
value: appCountCompact(input.appCount),
|
||||||
|
tone: input.appCount > 0 ? 'ok' : 'warning',
|
||||||
|
details: [
|
||||||
|
input.appCount > 0 ? appCountText(input.appCount) : 'Добавь хотя бы одно приложение на вкладке ProxiFyre.',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (flow === 'idle') {
|
||||||
|
return [
|
||||||
|
appSegment,
|
||||||
|
{
|
||||||
|
id: 'idle',
|
||||||
|
label: 'Трафик',
|
||||||
|
value: input.isDetectingComponents ? 'проверяю' : 'ожидает приложения',
|
||||||
|
tone: input.isDetectingComponents ? 'checking' : 'warning',
|
||||||
|
details: input.isDetectingComponents
|
||||||
|
? ['Проверяю ProxiFyre и текущий маршрут.']
|
||||||
|
: ['Пока нет выбранных приложений, ProxyWarden ничего не маршрутизирует.'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
appSegment,
|
||||||
|
{
|
||||||
|
id: 'direct',
|
||||||
|
label: 'Интернет',
|
||||||
|
value: 'напрямую',
|
||||||
|
tone: 'warning',
|
||||||
|
details: [
|
||||||
|
input.proxyfier?.installed
|
||||||
|
? 'ProxiFyre остановлен, поэтому выбранные приложения не перехватываются.'
|
||||||
|
: 'ProxiFyre не найден, поэтому выбранные приложения не перехватываются.',
|
||||||
|
'Пакеты идут обычным системным маршрутом без SOCKS5.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function routeChainSegments(input: RouteChainInput): RouteChainSegment[] {
|
function routeChainSegments(input: RouteChainInput): RouteChainSegment[] {
|
||||||
const proxyValidation = input.routeMode === 'external' && input.proxyInput.trim()
|
const proxyValidation = input.routeMode === 'external' && input.proxyInput.trim()
|
||||||
? safeProxyError(input.proxyInput)
|
? safeProxyError(input.proxyInput)
|
||||||
|
|||||||
BIN
src/assets/proxywarden-toggle-off.png
Normal file
BIN
src/assets/proxywarden-toggle-off.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/proxywarden-toggle-on.png
Normal file
BIN
src/assets/proxywarden-toggle-on.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 873 KiB |
@@ -3,7 +3,7 @@
|
|||||||
--app-change-dock-height: 0px;
|
--app-change-dock-height: 0px;
|
||||||
--app-admin-prompt-height: 0px;
|
--app-admin-prompt-height: 0px;
|
||||||
--change-row-count: 1;
|
--change-row-count: 1;
|
||||||
--app-header-row-height: 50px;
|
--app-header-row-height: 0px;
|
||||||
--app-tab-height: 46px;
|
--app-tab-height: 46px;
|
||||||
--app-header-height: calc(var(--app-header-row-height) + var(--app-tab-height));
|
--app-header-height: calc(var(--app-header-row-height) + var(--app-tab-height));
|
||||||
--motion-fast: 120ms;
|
--motion-fast: 120ms;
|
||||||
@@ -776,7 +776,8 @@ button:disabled {
|
|||||||
|
|
||||||
.simple-panel {
|
.simple-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
grid-template-rows: minmax(0, 1fr);
|
||||||
|
align-content: stretch;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: calc(100vh - var(--app-header-height) - var(--app-footer-height) - var(--app-change-dock-height) - var(--app-admin-prompt-height));
|
height: calc(100vh - var(--app-header-height) - var(--app-footer-height) - var(--app-change-dock-height) - var(--app-admin-prompt-height));
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -973,6 +974,8 @@ button:disabled {
|
|||||||
|
|
||||||
.tab-panel-frame {
|
.tab-panel-frame {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
will-change: opacity, transform;
|
will-change: opacity, transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1054,98 +1057,133 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.summary-panel {
|
.summary-panel {
|
||||||
gap: 12px;
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
align-content: stretch;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: clamp(12px, 3vh, 28px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-brand-title {
|
||||||
|
margin: 0;
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: clamp(22px, 4vw, 34px);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.05;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-main {
|
||||||
|
align-self: stretch;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 1fr) minmax(220px, 288px);
|
||||||
|
gap: clamp(22px, 5vw, 54px);
|
||||||
|
align-items: center;
|
||||||
|
width: min(100%, 760px);
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 18px clamp(34px, 10vh, 92px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-status-control {
|
.summary-status-control {
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
align-content: center;
|
||||||
gap: 12px;
|
align-self: center;
|
||||||
align-items: center;
|
justify-items: center;
|
||||||
border: 1px solid #2b3342;
|
gap: 14px;
|
||||||
border-radius: 4px;
|
width: 100%;
|
||||||
background: #151923;
|
overflow: visible;
|
||||||
padding: 13px 14px;
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-status-control > :not(.ui-busy-ring) {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-status-control.ok {
|
|
||||||
border-color: rgba(34, 197, 94, 0.44);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-status-control.warning {
|
|
||||||
border-color: rgba(245, 158, 11, 0.48);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-status-control.error {
|
|
||||||
border-color: rgba(239, 68, 68, 0.52);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-status-control.checking {
|
|
||||||
border-color: rgba(59, 130, 246, 0.54);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-status-dot {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #64748b;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-status-dot.ok {
|
|
||||||
background: #22c55e;
|
|
||||||
box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-status-dot.warning {
|
|
||||||
background: #f59e0b;
|
|
||||||
box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-status-dot.error {
|
|
||||||
background: #ef4444;
|
|
||||||
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-status-dot.checking {
|
|
||||||
background: #60a5fa;
|
|
||||||
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-status-copy span,
|
|
||||||
.summary-card-label {
|
.summary-card-label {
|
||||||
color: #8d99ae;
|
color: #8d99ae;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-status-copy {
|
.summary-toggle-button {
|
||||||
min-width: 0;
|
appearance: none;
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: clamp(180px, 30vw, 268px);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow:
|
||||||
|
0 22px 54px rgba(0, 0, 0, 0.48),
|
||||||
|
0 0 38px rgba(59, 130, 246, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
transition:
|
||||||
|
border-color var(--motion-fast) var(--ease-out),
|
||||||
|
box-shadow var(--motion-fast) var(--ease-out),
|
||||||
|
opacity var(--motion-fast) var(--ease-out),
|
||||||
|
transform var(--motion-fast) var(--ease-out);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-status-copy strong {
|
.summary-toggle-button:hover:not(:disabled) {
|
||||||
|
box-shadow:
|
||||||
|
0 26px 62px rgba(0, 0, 0, 0.54),
|
||||||
|
0 0 52px rgba(59, 130, 246, 0.18);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-toggle-button:active:not(:disabled) {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-toggle-button:focus-visible {
|
||||||
|
outline: 0;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2px rgba(59, 130, 246, 0.72),
|
||||||
|
0 22px 54px rgba(0, 0, 0, 0.48);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-toggle-button > :not(.ui-busy-ring) {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-toggle-button img {
|
||||||
display: block;
|
display: block;
|
||||||
color: #f8fafc;
|
width: 100%;
|
||||||
font-size: 22px;
|
height: 100%;
|
||||||
line-height: 1.15;
|
border-radius: 50%;
|
||||||
margin-top: 2px;
|
object-fit: cover;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: scale(1.18);
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-status-copy p {
|
.summary-state-label {
|
||||||
color: #b6c2d4;
|
color: #e5e7eb;
|
||||||
margin: 4px 0 0;
|
font-size: clamp(22px, 3.4vw, 34px);
|
||||||
overflow-wrap: anywhere;
|
font-weight: 800;
|
||||||
|
line-height: 1.1;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-status-button {
|
.summary-state-label.ok {
|
||||||
min-width: 148px;
|
color: #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-state-label.warning,
|
||||||
|
.summary-state-label.checking {
|
||||||
|
color: #fcd34d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-state-label.error {
|
||||||
|
color: #fecaca;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-readout {
|
.summary-readout {
|
||||||
@@ -1983,12 +2021,90 @@ button.summary-card:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.route-chain {
|
.route-chain {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.route-chain--vertical {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 288px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-chain-rail {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 0;
|
||||||
|
top: 28px;
|
||||||
|
bottom: 28px;
|
||||||
|
left: 13px;
|
||||||
|
width: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(96, 165, 250, 0.08),
|
||||||
|
rgba(96, 165, 250, 0.34) 28%,
|
||||||
|
rgba(34, 197, 94, 0.28) 72%,
|
||||||
|
rgba(96, 165, 250, 0.08)
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-chain--direct .route-chain-rail {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(245, 158, 11, 0.08),
|
||||||
|
rgba(245, 158, 11, 0.38) 32%,
|
||||||
|
rgba(245, 158, 11, 0.3) 68%,
|
||||||
|
rgba(245, 158, 11, 0.08)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-chain--idle .route-chain-rail {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(100, 116, 139, 0.06),
|
||||||
|
rgba(100, 116, 139, 0.24) 48%,
|
||||||
|
rgba(100, 116, 139, 0.06)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-chain-packet {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
left: 50%;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #93c5fd;
|
||||||
|
box-shadow:
|
||||||
|
0 0 8px rgba(96, 165, 250, 0.95),
|
||||||
|
0 0 16px rgba(34, 197, 94, 0.35);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
animation: route-packet-flow 2.7s linear infinite;
|
||||||
|
animation-delay: -0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-chain--direct .route-chain-packet {
|
||||||
|
background: #fcd34d;
|
||||||
|
box-shadow:
|
||||||
|
0 0 8px rgba(245, 158, 11, 0.9),
|
||||||
|
0 0 16px rgba(245, 158, 11, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-chain-packet--two {
|
||||||
|
animation-delay: -1.05s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-chain-packet--three {
|
||||||
|
animation-delay: -1.95s;
|
||||||
|
}
|
||||||
|
|
||||||
.route-chain-segment {
|
.route-chain-segment {
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
@@ -2010,6 +2126,17 @@ button.summary-card:hover {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.route-chain--vertical .route-chain-segment {
|
||||||
|
z-index: 1;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
grid-template-areas:
|
||||||
|
"dot label"
|
||||||
|
"dot value";
|
||||||
|
min-height: 58px;
|
||||||
|
background: rgba(17, 23, 32, 0.94);
|
||||||
|
padding: 9px 10px 9px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
.route-chain-segment > :not(.ui-busy-ring) {
|
.route-chain-segment > :not(.ui-busy-ring) {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -2052,6 +2179,22 @@ button.summary-card:hover {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.route-chain--vertical .route-chain-arrow {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
bottom: -17px;
|
||||||
|
left: 9px;
|
||||||
|
color: #60a5fa;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
text-shadow: 0 0 8px rgba(96, 165, 250, 0.52);
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-chain--direct .route-chain-arrow {
|
||||||
|
color: #f59e0b;
|
||||||
|
text-shadow: 0 0 8px rgba(245, 158, 11, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
.route-chain-segment.ok .route-chain-dot {
|
.route-chain-segment.ok .route-chain-dot {
|
||||||
background: #22c55e;
|
background: #22c55e;
|
||||||
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.12);
|
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.12);
|
||||||
@@ -2774,6 +2917,23 @@ button.summary-card:hover {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes route-packet-flow {
|
||||||
|
0% {
|
||||||
|
top: -8px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
8%,
|
||||||
|
88% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.tab-panel-frame,
|
.tab-panel-frame,
|
||||||
.tab-panel,
|
.tab-panel,
|
||||||
@@ -2784,7 +2944,8 @@ button.summary-card:hover {
|
|||||||
.finder-border-glow-segment,
|
.finder-border-glow-segment,
|
||||||
.ui-service-border-glow-segment,
|
.ui-service-border-glow-segment,
|
||||||
.status-light,
|
.status-light,
|
||||||
.summary-status-dot.checking {
|
.summary-toggle-button,
|
||||||
|
.route-chain-packet {
|
||||||
animation: none;
|
animation: none;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
@@ -2900,19 +3061,22 @@ button.summary-card:hover {
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-status-control {
|
.summary-main {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
align-items: stretch;
|
align-content: center;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 22px;
|
||||||
|
padding: 0 10px clamp(24px, 8vh, 64px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-status-dot {
|
.summary-status-control {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-status-button {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary-toggle-button {
|
||||||
|
width: min(202px, 64vw);
|
||||||
|
}
|
||||||
|
|
||||||
.setup-strip {
|
.setup-strip {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@@ -2962,6 +3126,11 @@ button.summary-card:hover {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.route-chain--vertical {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
width: min(100%, 300px);
|
||||||
|
}
|
||||||
|
|
||||||
.add-toolbar {
|
.add-toolbar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user