Update VPN client and gateway behavior
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.23.7',
|
macClient: '0.23.8',
|
||||||
gatewayClient: '0.24.7',
|
gatewayClient: '0.24.8',
|
||||||
gatewayBackend: '0.24.0',
|
gatewayBackend: '0.24.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useState, type ReactNode } from 'react';
|
import { useState, type ReactNode } from 'react';
|
||||||
import { ConfirmationDialog } from '../../ui/ConfirmationDialog.js';
|
import { ConfirmationDialog } from '../../ui/ConfirmationDialog.js';
|
||||||
|
import { CopyButton } from '../../ui/CopyButton.js';
|
||||||
|
import { Tooltip } from '../../ui/Tooltip.js';
|
||||||
import {
|
import {
|
||||||
connectionAction,
|
connectionAction,
|
||||||
connectionDurationParts,
|
connectionDurationParts,
|
||||||
@@ -170,9 +172,9 @@ export function ConnectionPanel({
|
|||||||
>
|
>
|
||||||
{brandSlot}
|
{brandSlot}
|
||||||
{powerButton}
|
{powerButton}
|
||||||
{powerUnavailable && <span className="client-tooltip" id="gateway-power-unavailable" role="tooltip">
|
{powerUnavailable && <Tooltip id="gateway-power-unavailable">
|
||||||
{remoteOwned ? 'Подключением управляет Harbor Gateway' : 'Сначала добавьте подписку и выберите сервер'}
|
{remoteOwned ? 'Подключением управляет Harbor Gateway' : 'Сначала добавьте подписку и выберите сервер'}
|
||||||
</span>}
|
</Tooltip>}
|
||||||
</div>
|
</div>
|
||||||
<div className="client-state-detail">
|
<div className="client-state-detail">
|
||||||
{connected && !remoteOwned ? (
|
{connected && !remoteOwned ? (
|
||||||
@@ -216,9 +218,9 @@ export function ConnectionPanel({
|
|||||||
</span>
|
</span>
|
||||||
</time>
|
</time>
|
||||||
</span>
|
</span>
|
||||||
<span className="client-tooltip" role="tooltip">
|
<Tooltip>
|
||||||
{durationMode === 'digital' ? 'Показать время словами' : 'Показать цифровой таймер'}
|
{durationMode === 'digital' ? 'Показать время словами' : 'Показать цифровой таймер'}
|
||||||
</span>
|
</Tooltip>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<p key="hint">
|
<p key="hint">
|
||||||
@@ -254,16 +256,13 @@ export function ConnectionPanel({
|
|||||||
<div className="client-proxy-actions">
|
<div className="client-proxy-actions">
|
||||||
{proxyKinds.map(([kind, label]) => {
|
{proxyKinds.map(([kind, label]) => {
|
||||||
const feedback = copyFeedback?.[kind];
|
const feedback = copyFeedback?.[kind];
|
||||||
return <button
|
return <CopyButton
|
||||||
className={`client-copy-button${feedback ? feedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
label={label}
|
||||||
type="button"
|
feedback={feedback}
|
||||||
key={kind}
|
key={kind}
|
||||||
aria-label={`Скопировать ${label}: ${kind === 'gateway' ? gatewayAddress : proxyUrls[kind]}`}
|
ariaLabel={`Скопировать ${label}: ${kind === 'gateway' ? gatewayAddress : proxyUrls[kind]}`}
|
||||||
onClick={() => onCopyProxy(kind)}
|
onClick={() => onCopyProxy(kind)}
|
||||||
>
|
/>;
|
||||||
<span className="client-copy-label">{label}</span>
|
|
||||||
{feedback && <span className="client-copy-feedback" aria-hidden="true" key={feedback.cycle}>{feedback.failed ? 'Ошибка' : 'Скопировано'}</span>}
|
|
||||||
</button>;
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { RailAction } from '../../ui/RailAction.js';
|
||||||
import {
|
import {
|
||||||
formatByteString,
|
formatByteString,
|
||||||
formatLastSeen,
|
formatLastSeen,
|
||||||
@@ -191,13 +192,13 @@ export function useDevicesFeature({
|
|||||||
export type DevicesFeature = ReturnType<typeof useDevicesFeature>;
|
export type DevicesFeature = ReturnType<typeof useDevicesFeature>;
|
||||||
|
|
||||||
export function DevicesToggle({ feature, onToggle }: { feature: DevicesFeature; onToggle: () => void }) {
|
export function DevicesToggle({ feature, onToggle }: { feature: DevicesFeature; onToggle: () => void }) {
|
||||||
return <button
|
return <RailAction
|
||||||
ref={feature.toggleRef}
|
buttonRef={feature.toggleRef}
|
||||||
className={`client-instructions-toggle client-devices-toggle${feature.isOpen ? ' is-open' : ''}`}
|
className="client-instructions-toggle client-devices-toggle"
|
||||||
type="button"
|
open={feature.isOpen}
|
||||||
aria-expanded={feature.isOpen}
|
controls="client-devices"
|
||||||
aria-controls="client-devices"
|
ariaLabel={feature.isOpen ? 'Закрыть устройства' : 'Устройства Gateway'}
|
||||||
aria-label={feature.isOpen ? 'Закрыть устройства' : 'Устройства Gateway'}
|
label="Устройства"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
@@ -205,8 +206,7 @@ export function DevicesToggle({ feature, onToggle }: { feature: DevicesFeature;
|
|||||||
<rect className="client-rail-device-secondary" x="13.5" y="8" width="7" height="7" rx="1.5" />
|
<rect className="client-rail-device-secondary" x="13.5" y="8" width="7" height="7" rx="1.5" />
|
||||||
<path className="client-rail-device-link" d="M6 19h12M7 15v4M17 15v4" />
|
<path className="client-rail-device-link" d="M6 19h12M7 15v4M17 15v4" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Устройства</span>
|
</RailAction>;
|
||||||
</button>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GatewayTrafficSummary({ feature, now }: { feature: DevicesFeature; now: number }) {
|
export function GatewayTrafficSummary({ feature, now }: { feature: DevicesFeature; now: number }) {
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type CSSProperties,
|
type CSSProperties,
|
||||||
type ReactNode,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { Drawer } from '../../ui/Drawer.js';
|
||||||
|
import { Tooltip } from '../../ui/Tooltip.js';
|
||||||
import { copyText } from '../../utils/clientControls.js';
|
import { copyText } from '../../utils/clientControls.js';
|
||||||
import {
|
import {
|
||||||
byteString,
|
byteString,
|
||||||
@@ -40,10 +41,6 @@ function requestMessage(value: unknown) {
|
|||||||
return typeof message === 'string' ? message : undefined;
|
return typeof message === 'string' ? message : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tooltip({ children }: { children: ReactNode }) {
|
|
||||||
return <span className="client-tooltip" role="tooltip">{children}</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TextMorph({ from, to }: { from: string; to: string }) {
|
function TextMorph({ from, to }: { from: string; to: string }) {
|
||||||
const anchor = from.length >= to.length ? from : to;
|
const anchor = from.length >= to.length ? from : to;
|
||||||
return <span className="client-text-morph" aria-hidden="true">
|
return <span className="client-text-morph" aria-hidden="true">
|
||||||
@@ -280,22 +277,17 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<Drawer
|
||||||
ref={panelRef}
|
panelRef={panelRef}
|
||||||
|
closeRef={closeRef}
|
||||||
id="client-devices"
|
id="client-devices"
|
||||||
className={`client-drawer client-instructions client-devices${open ? ' is-open' : ''}`}
|
className="client-instructions client-devices"
|
||||||
aria-labelledby="client-devices-title"
|
sheetClassName="client-instructions-sheet client-devices-sheet"
|
||||||
aria-hidden={!open}
|
open={open}
|
||||||
inert={!open ? true : undefined}
|
labelledBy="client-devices-title"
|
||||||
|
closeLabel="Закрыть устройства"
|
||||||
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div className="client-drawer-sheet client-instructions-sheet client-devices-sheet">
|
|
||||||
<button
|
|
||||||
ref={closeRef}
|
|
||||||
className="client-drawer-close"
|
|
||||||
type="button"
|
|
||||||
aria-label="Закрыть устройства"
|
|
||||||
onClick={onClose}
|
|
||||||
>×</button>
|
|
||||||
<header className="client-instructions-header client-devices-header">
|
<header className="client-instructions-header client-devices-header">
|
||||||
<div className="client-devices-kicker">
|
<div className="client-devices-kicker">
|
||||||
<span>Gateway · {devices.length}</span>
|
<span>Gateway · {devices.length}</span>
|
||||||
@@ -583,7 +575,6 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
</article>;
|
</article>;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Drawer>
|
||||||
</aside>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
type FormEvent,
|
type FormEvent,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
import { flushSync } from 'react-dom';
|
||||||
|
import { Drawer } from '../../ui/Drawer.js';
|
||||||
|
import { Tooltip } from '../../ui/Tooltip.js';
|
||||||
import {
|
import {
|
||||||
CONNECTIVITY_IP_SOURCES,
|
CONNECTIVITY_IP_SOURCES,
|
||||||
CONNECTIVITY_SITES,
|
CONNECTIVITY_SITES,
|
||||||
@@ -324,23 +326,19 @@ export function ConnectivityDiagnosticsPanel({
|
|||||||
const { isOpen: open, panelRef, closeRef, close: onClose } = feature;
|
const { isOpen: open, panelRef, closeRef, close: onClose } = feature;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<Drawer
|
||||||
ref={panelRef}
|
panelRef={panelRef}
|
||||||
|
closeRef={closeRef}
|
||||||
|
sheetRef={sheetRef}
|
||||||
id="client-diagnostics"
|
id="client-diagnostics"
|
||||||
className={`client-drawer client-instructions client-diagnostics${open ? ' is-open' : ''}`}
|
className="client-instructions client-diagnostics"
|
||||||
aria-labelledby="client-diagnostics-title"
|
sheetClassName="client-instructions-sheet client-diagnostics-sheet"
|
||||||
aria-hidden={!open}
|
open={open}
|
||||||
inert={!open ? true : undefined}
|
labelledBy="client-diagnostics-title"
|
||||||
|
closeLabel="Закрыть диагностику"
|
||||||
|
onClose={onClose}
|
||||||
|
leading={<span ref={runnerRef} className="client-diagnostics-active-marker" aria-hidden="true" />}
|
||||||
>
|
>
|
||||||
<div ref={sheetRef} className="client-drawer-sheet client-instructions-sheet client-diagnostics-sheet">
|
|
||||||
<span ref={runnerRef} className="client-diagnostics-active-marker" aria-hidden="true" />
|
|
||||||
<button
|
|
||||||
ref={closeRef}
|
|
||||||
className="client-drawer-close"
|
|
||||||
type="button"
|
|
||||||
aria-label="Закрыть диагностику"
|
|
||||||
onClick={onClose}
|
|
||||||
>×</button>
|
|
||||||
<header className="client-instructions-header client-diagnostics-header">
|
<header className="client-instructions-header client-diagnostics-header">
|
||||||
<span>{isGateway ? 'Gateway' : 'Connect'} · Direct ↔ VPN</span>
|
<span>{isGateway ? 'Gateway' : 'Connect'} · Direct ↔ VPN</span>
|
||||||
<div className="client-diagnostics-title-row">
|
<div className="client-diagnostics-title-row">
|
||||||
@@ -358,7 +356,7 @@ export function ConnectivityDiagnosticsPanel({
|
|||||||
<path d="M20 11a8 8 0 1 0-2.3 6.7M20 5v6h-6" />
|
<path d="M20 11a8 8 0 1 0-2.3 6.7M20 5v6h-6" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span className="client-tooltip" role="tooltip">Проверить маршруты</span>
|
<Tooltip>Проверить маршруты</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -420,7 +418,7 @@ export function ConnectivityDiagnosticsPanel({
|
|||||||
<span role="cell"><Status value={resultStatus(direct, running)} route={`Напрямую, ${site.label}`} /></span>
|
<span role="cell"><Status value={resultStatus(direct, running)} route={`Напрямую, ${site.label}`} /></span>
|
||||||
<span role="cell"><Status value={resultStatus(vpn, running, result?.vpn?.available !== false)} route={`VPN, ${site.label}`} /></span>
|
<span role="cell"><Status value={resultStatus(vpn, running, result?.vpn?.available !== false)} route={`VPN, ${site.label}`} /></span>
|
||||||
<button
|
<button
|
||||||
className="client-local-rule-delete"
|
className="client-row-delete"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`Удалить сервис ${site.label}`}
|
aria-label={`Удалить сервис ${site.label}`}
|
||||||
disabled={serviceEditorBlocked}
|
disabled={serviceEditorBlocked}
|
||||||
@@ -466,7 +464,7 @@ export function ConnectivityDiagnosticsPanel({
|
|||||||
<button type="submit" disabled={serviceEditorBlocked}>Добавить</button>
|
<button type="submit" disabled={serviceEditorBlocked}>Добавить</button>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
className="client-local-rule-delete"
|
className="client-row-delete"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Отменить добавление сервиса"
|
aria-label="Отменить добавление сервиса"
|
||||||
onClick={() => removeService('draft')}
|
onClick={() => removeService('draft')}
|
||||||
@@ -480,9 +478,9 @@ export function ConnectivityDiagnosticsPanel({
|
|||||||
|
|
||||||
{!sites.length && !adding && <p className="client-diagnostics-services-empty">Сервисов пока нет.</p>}
|
{!sites.length && !adding && <p className="client-diagnostics-services-empty">Сервисов пока нет.</p>}
|
||||||
|
|
||||||
<div className="client-local-rule-add-slot client-diagnostics-add-slot">
|
<div className="client-row-add-slot client-diagnostics-add-slot">
|
||||||
<button
|
<button
|
||||||
className="client-local-rule-add"
|
className="client-row-add"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={serviceEditorBlocked || adding || customServices.length >= MAX_CUSTOM_DIAGNOSTIC_SERVICES}
|
disabled={serviceEditorBlocked || adding || customServices.length >= MAX_CUSTOM_DIAGNOSTIC_SERVICES}
|
||||||
onClick={() => setAdding(true)}
|
onClick={() => setAdding(true)}
|
||||||
@@ -493,7 +491,6 @@ export function ConnectivityDiagnosticsPanel({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</Drawer>
|
||||||
</aside>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { RailAction } from '../../ui/RailAction.js';
|
||||||
|
|
||||||
export function useDiagnosticsFeature() {
|
export function useDiagnosticsFeature() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -47,19 +48,18 @@ export function DiagnosticsToggle({
|
|||||||
feature: DiagnosticsFeature;
|
feature: DiagnosticsFeature;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}) {
|
}) {
|
||||||
return <button
|
return <RailAction
|
||||||
ref={feature.toggleRef}
|
buttonRef={feature.toggleRef}
|
||||||
className={`client-instructions-toggle client-diagnostics-toggle${feature.isOpen ? ' is-open' : ''}`}
|
className="client-instructions-toggle client-diagnostics-toggle"
|
||||||
type="button"
|
open={feature.isOpen}
|
||||||
aria-expanded={feature.isOpen}
|
controls="client-diagnostics"
|
||||||
aria-controls="client-diagnostics"
|
ariaLabel={feature.isOpen ? 'Закрыть диагностику' : 'Проверить маршруты'}
|
||||||
aria-label={feature.isOpen ? 'Закрыть диагностику' : 'Проверить маршруты'}
|
label="Диагностика"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path className="client-rail-diagnostics-base" d="M3 12h4l2.2-5 4.2 10 2.1-5H21" />
|
<path className="client-rail-diagnostics-base" d="M3 12h4l2.2-5 4.2 10 2.1-5H21" />
|
||||||
<path className="client-rail-diagnostics-pulse" pathLength="1" d="M3 12h4l2.2-5 4.2 10 2.1-5H21" />
|
<path className="client-rail-diagnostics-pulse" pathLength="1" d="M3 12h4l2.2-5 4.2 10 2.1-5H21" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Диагностика</span>
|
</RailAction>;
|
||||||
</button>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
import { flushSync } from 'react-dom';
|
||||||
|
import { CopyButton } from '../../ui/CopyButton.js';
|
||||||
|
import { Drawer } from '../../ui/Drawer.js';
|
||||||
|
import { RailAction } from '../../ui/RailAction.js';
|
||||||
import { copyText } from '../../utils/clientControls.js';
|
import { copyText } from '../../utils/clientControls.js';
|
||||||
import { instructionBlocks } from './instructionBlocks.js';
|
import { instructionBlocks } from './instructionBlocks.js';
|
||||||
|
|
||||||
@@ -135,16 +138,12 @@ function InstructionBlock({
|
|||||||
const feedback = copyFeedback[action.id];
|
const feedback = copyFeedback[action.id];
|
||||||
return <div className="client-instruction-copy" key={action.id}>
|
return <div className="client-instruction-copy" key={action.id}>
|
||||||
<span>{action.label}</span>
|
<span>{action.label}</span>
|
||||||
<button
|
<CopyButton
|
||||||
className={`client-copy-button client-instruction-copy-button${feedback ? feedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
className="client-instruction-copy-button"
|
||||||
type="button"
|
label="Скопировать"
|
||||||
|
feedback={feedback}
|
||||||
onClick={() => copyInstruction(action)}
|
onClick={() => copyInstruction(action)}
|
||||||
>
|
/>
|
||||||
<span className="client-copy-label">Скопировать</span>
|
|
||||||
{feedback && <span className="client-copy-feedback" aria-hidden="true">
|
|
||||||
{feedback.failed ? 'Ошибка' : 'Скопировано'}
|
|
||||||
</span>}
|
|
||||||
</button>
|
|
||||||
</div>;
|
</div>;
|
||||||
})}
|
})}
|
||||||
<span className="client-live-region" role="status" aria-live="polite">
|
<span className="client-live-region" role="status" aria-live="polite">
|
||||||
@@ -238,21 +237,20 @@ export function InstructionsToggle({
|
|||||||
feature: InstructionsFeature;
|
feature: InstructionsFeature;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}) {
|
}) {
|
||||||
return <button
|
return <RailAction
|
||||||
ref={feature.toggleRef}
|
buttonRef={feature.toggleRef}
|
||||||
className={`client-instructions-toggle${feature.isOpen ? ' is-open' : ''}`}
|
className="client-instructions-toggle"
|
||||||
type="button"
|
open={feature.isOpen}
|
||||||
aria-expanded={feature.isOpen}
|
controls="client-instructions"
|
||||||
aria-controls="client-instructions"
|
ariaLabel={feature.isOpen ? 'Закрыть инструкции' : 'Как использовать'}
|
||||||
aria-label={feature.isOpen ? 'Закрыть инструкции' : 'Как использовать'}
|
label="Как использовать"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<circle className="client-rail-info-ring" cx="12" cy="12" r="8.5" pathLength="1" />
|
<circle className="client-rail-info-ring" cx="12" cy="12" r="8.5" pathLength="1" />
|
||||||
<path d="M12 11v5M12 8h.01" />
|
<path d="M12 11v5M12 8h.01" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Как использовать</span>
|
</RailAction>;
|
||||||
</button>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InstructionsPanel({
|
export function InstructionsPanel({
|
||||||
@@ -262,22 +260,17 @@ export function InstructionsPanel({
|
|||||||
feature: InstructionsFeature;
|
feature: InstructionsFeature;
|
||||||
isGateway: boolean;
|
isGateway: boolean;
|
||||||
}) {
|
}) {
|
||||||
return <aside
|
return <Drawer
|
||||||
ref={feature.panelRef}
|
panelRef={feature.panelRef}
|
||||||
|
closeRef={feature.closeRef}
|
||||||
id="client-instructions"
|
id="client-instructions"
|
||||||
className={`client-drawer client-instructions${feature.isOpen ? ' is-open' : ''}`}
|
className="client-instructions"
|
||||||
aria-labelledby="instructions-title"
|
sheetClassName="client-instructions-sheet"
|
||||||
aria-hidden={!feature.isOpen}
|
open={feature.isOpen}
|
||||||
inert={!feature.isOpen ? true : undefined}
|
labelledBy="instructions-title"
|
||||||
|
closeLabel="Закрыть инструкции"
|
||||||
|
onClose={feature.close}
|
||||||
>
|
>
|
||||||
<div className="client-drawer-sheet client-instructions-sheet">
|
|
||||||
<button
|
|
||||||
ref={feature.closeRef}
|
|
||||||
className="client-drawer-close"
|
|
||||||
type="button"
|
|
||||||
aria-label="Закрыть инструкции"
|
|
||||||
onClick={feature.close}
|
|
||||||
>×</button>
|
|
||||||
<header className="client-instructions-header">
|
<header className="client-instructions-header">
|
||||||
<span>Подключение</span>
|
<span>Подключение</span>
|
||||||
<h2 id="instructions-title">Как использовать {isGateway ? 'Gateway' : 'прокси'}</h2>
|
<h2 id="instructions-title">Как использовать {isGateway ? 'Gateway' : 'прокси'}</h2>
|
||||||
@@ -296,6 +289,5 @@ export function InstructionsPanel({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Drawer>;
|
||||||
</aside>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
type FormEvent,
|
type FormEvent,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
type RefObject,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
import { flushSync } from 'react-dom';
|
||||||
import { canAppendRouteRule } from '../../../shared/routingRules.js';
|
import { canAppendRouteRule } from '../../../shared/routingRules.js';
|
||||||
import type { RouteRule } from '../../../shared/contracts/state.js';
|
import type { RouteRule } from '../../../shared/contracts/state.js';
|
||||||
import { operationBlocked } from '../../state/operations.js';
|
import { operationBlocked } from '../../state/operations.js';
|
||||||
import { ConfirmationDialog } from '../../ui/ConfirmationDialog.js';
|
import { ConfirmationDialog } from '../../ui/ConfirmationDialog.js';
|
||||||
|
import { Drawer } from '../../ui/Drawer.js';
|
||||||
|
import { RailAction } from '../../ui/RailAction.js';
|
||||||
|
|
||||||
const ROUTE_RULE_OPTIONS: Array<[RouteRule['type'], string]> = [
|
const ROUTE_RULE_OPTIONS: Array<[RouteRule['type'], string]> = [
|
||||||
['domain', 'Точный домен'],
|
['domain', 'Точный домен'],
|
||||||
@@ -393,18 +394,22 @@ export function RoutingToggle({
|
|||||||
}) {
|
}) {
|
||||||
const disabled = gatewayDirect || (isGateway && !hasSubscription);
|
const disabled = gatewayDirect || (isGateway && !hasSubscription);
|
||||||
return (
|
return (
|
||||||
<button
|
<RailAction
|
||||||
ref={feature.toggleRef}
|
buttonRef={feature.toggleRef}
|
||||||
className={`client-local-rules-toggle${feature.isOpen ? ' is-open' : ''}${feature.pendingRestart ? ' has-pending' : ''}`}
|
className={`client-local-rules-toggle${feature.pendingRestart ? ' has-pending' : ''}`}
|
||||||
type="button"
|
open={feature.isOpen}
|
||||||
|
controls="client-local-rules"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
aria-expanded={feature.isOpen}
|
ariaLabel={disabled
|
||||||
aria-controls="client-local-rules"
|
|
||||||
aria-label={disabled
|
|
||||||
? hasSubscription
|
? hasSubscription
|
||||||
? 'Локальные правила недоступны: сейчас работают правила Harbor Gateway'
|
? 'Локальные правила недоступны: сейчас работают правила Harbor Gateway'
|
||||||
: 'Локальные правила недоступны: сначала добавьте подписку'
|
: 'Локальные правила недоступны: сначала добавьте подписку'
|
||||||
: feature.isOpen ? 'Закрыть локальные правила' : 'Настроить локальные правила'}
|
: feature.isOpen ? 'Закрыть локальные правила' : 'Настроить локальные правила'}
|
||||||
|
label={disabled
|
||||||
|
? hasSubscription
|
||||||
|
? 'Локальные правила недоступны: сейчас работают правила Gateway'
|
||||||
|
: 'Сначала добавьте подписку'
|
||||||
|
: feature.pendingRestart ? 'Правила ждут перезапуска' : 'Локальные правила'}
|
||||||
onClick={() => feature.isOpen ? feature.requestClose() : onOpen()}
|
onClick={() => feature.isOpen ? feature.requestClose() : onOpen()}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
@@ -412,12 +417,7 @@ export function RoutingToggle({
|
|||||||
<circle className="client-rail-rule-knob is-top" cx="15" cy="7" r="2" />
|
<circle className="client-rail-rule-knob is-top" cx="15" cy="7" r="2" />
|
||||||
<circle className="client-rail-rule-knob is-bottom" cx="9" cy="17" r="2" />
|
<circle className="client-rail-rule-knob is-bottom" cx="9" cy="17" r="2" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>{disabled
|
</RailAction>
|
||||||
? hasSubscription
|
|
||||||
? 'Локальные правила недоступны: сейчас работают правила Gateway'
|
|
||||||
: 'Сначала добавьте подписку'
|
|
||||||
: feature.pendingRestart ? 'Правила ждут перезапуска' : 'Локальные правила'}</span>
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,22 +446,17 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
|||||||
const incomplete = draftRules.some((rule) => !String(rule.value || '').trim());
|
const incomplete = draftRules.some((rule) => !String(rule.value || '').trim());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<Drawer
|
||||||
ref={feature.panelRef as RefObject<HTMLElement>}
|
panelRef={feature.panelRef}
|
||||||
|
closeRef={feature.closeRef}
|
||||||
id="client-local-rules"
|
id="client-local-rules"
|
||||||
className={`client-drawer client-local-rules${feature.isOpen ? ' is-open' : ''}`}
|
className="client-local-rules"
|
||||||
aria-labelledby="local-rules-title"
|
sheetClassName="client-local-rules-sheet"
|
||||||
aria-hidden={!feature.isOpen}
|
open={feature.isOpen}
|
||||||
inert={!feature.isOpen ? true : undefined}
|
labelledBy="local-rules-title"
|
||||||
|
closeLabel="Закрыть локальные правила"
|
||||||
|
onClose={feature.requestClose}
|
||||||
>
|
>
|
||||||
<div className="client-drawer-sheet client-local-rules-sheet">
|
|
||||||
<button
|
|
||||||
ref={feature.closeRef}
|
|
||||||
className="client-drawer-close"
|
|
||||||
type="button"
|
|
||||||
aria-label="Закрыть локальные правила"
|
|
||||||
onClick={feature.requestClose}
|
|
||||||
>×</button>
|
|
||||||
<header className="client-local-rules-header">
|
<header className="client-local-rules-header">
|
||||||
<span>Маршрутизация</span>
|
<span>Маршрутизация</span>
|
||||||
<button
|
<button
|
||||||
@@ -532,7 +527,7 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
|||||||
/>
|
/>
|
||||||
<span className="client-local-rule-status" role="status">{statusLabel}</span>
|
<span className="client-local-rule-status" role="status">{statusLabel}</span>
|
||||||
<button
|
<button
|
||||||
className="client-local-rule-delete"
|
className="client-row-delete"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`Удалить правило ${index + 1}`}
|
aria-label={`Удалить правило ${index + 1}`}
|
||||||
onClick={() => feature.remove(rule._key)}
|
onClick={() => feature.remove(rule._key)}
|
||||||
@@ -549,8 +544,8 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
|||||||
})}
|
})}
|
||||||
{!feature.rules.length && <p className="client-local-rules-empty">Правил пока нет. Весь трафик идёт через VPN.</p>}
|
{!feature.rules.length && <p className="client-local-rules-empty">Правил пока нет. Весь трафик идёт через VPN.</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="client-local-rule-add-slot">
|
<div className="client-row-add-slot">
|
||||||
<button className="client-local-rule-add" type="button" disabled={!canAdd} onClick={feature.add}>
|
<button className="client-row-add" type="button" disabled={!canAdd} onClick={feature.add}>
|
||||||
+ Добавить правило
|
+ Добавить правило
|
||||||
</button>
|
</button>
|
||||||
<span className={incomplete ? 'is-visible' : ''}>Сначала заполните текущее правило</span>
|
<span className={incomplete ? 'is-visible' : ''}>Сначала заполните текущее правило</span>
|
||||||
@@ -565,8 +560,7 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
|||||||
<button type="button" onClick={feature.requestClose}>Отмена</button>
|
<button type="button" onClick={feature.requestClose}>Отмена</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</Drawer>
|
||||||
</aside>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
} from '../../utils/clientControls.js';
|
} from '../../utils/clientControls.js';
|
||||||
import { formatBytes } from '../../utils/format.js';
|
import { formatBytes } from '../../utils/format.js';
|
||||||
import { ConfirmationDialog } from '../../ui/ConfirmationDialog.js';
|
import { ConfirmationDialog } from '../../ui/ConfirmationDialog.js';
|
||||||
|
import { Drawer } from '../../ui/Drawer.js';
|
||||||
|
import { RailAction } from '../../ui/RailAction.js';
|
||||||
import type { RequestError } from './requestError.js';
|
import type { RequestError } from './requestError.js';
|
||||||
|
|
||||||
interface ProfileError extends RequestError {
|
interface ProfileError extends RequestError {
|
||||||
@@ -75,7 +77,7 @@ export function useSubscriptionFeature({
|
|||||||
const [deleteId, setDeleteId] = useState('');
|
const [deleteId, setDeleteId] = useState('');
|
||||||
const [refreshingIds, setRefreshingIds] = useState<string[]>([]);
|
const [refreshingIds, setRefreshingIds] = useState<string[]>([]);
|
||||||
const [revealVersions, setRevealVersions] = useState<Record<string, number>>({});
|
const [revealVersions, setRevealVersions] = useState<Record<string, number>>({});
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const panelRef = useRef<HTMLElement>(null);
|
||||||
const toggleRef = useRef<HTMLButtonElement>(null);
|
const toggleRef = useRef<HTMLButtonElement>(null);
|
||||||
const closeRef = useRef<HTMLButtonElement>(null);
|
const closeRef = useRef<HTMLButtonElement>(null);
|
||||||
const labelRef = useRef<HTMLInputElement>(null);
|
const labelRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -307,20 +309,19 @@ export function SubscriptionToggle({
|
|||||||
feature: SubscriptionFeatureController;
|
feature: SubscriptionFeatureController;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}) {
|
}) {
|
||||||
return <button
|
return <RailAction
|
||||||
ref={feature.toggleRef}
|
buttonRef={feature.toggleRef}
|
||||||
className={`client-instructions-toggle client-subscription-toggle${feature.open ? ' is-open' : ''}`}
|
className="client-instructions-toggle client-subscription-toggle"
|
||||||
type="button"
|
open={feature.open}
|
||||||
aria-expanded={feature.open}
|
controls="client-subscription-drawer"
|
||||||
aria-controls="client-subscription-drawer"
|
ariaLabel={feature.open ? 'Закрыть подписки' : 'Управление подписками'}
|
||||||
aria-label={feature.open ? 'Закрыть подписки' : 'Управление подписками'}
|
label="Подписки"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path d="M5 5h14v14H5zM8 9h8M8 13h5" />
|
<path d="M5 5h14v14H5zM8 9h8M8 13h5" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Подписки</span>
|
</RailAction>;
|
||||||
</button>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddProfileForm({ feature }: { feature: SubscriptionFeatureController }) {
|
function AddProfileForm({ feature }: { feature: SubscriptionFeatureController }) {
|
||||||
@@ -514,24 +515,19 @@ export function SubscriptionPanel({
|
|||||||
<AddProfileForm feature={feature} />
|
<AddProfileForm feature={feature} />
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
<div
|
<Drawer
|
||||||
ref={feature.panelRef}
|
panelRef={feature.panelRef}
|
||||||
|
closeRef={feature.closeRef}
|
||||||
id="client-subscription-drawer"
|
id="client-subscription-drawer"
|
||||||
className={`client-drawer client-subscription-drawer${drawerOpen ? ' is-open' : ''}`}
|
className="client-subscription-drawer"
|
||||||
aria-label="Управление подписками"
|
sheetClassName="client-subscription-sheet"
|
||||||
aria-hidden={!drawerOpen}
|
open={drawerOpen}
|
||||||
inert={!drawerOpen ? true : undefined}
|
label="Управление подписками"
|
||||||
|
closeLabel="Закрыть подписки"
|
||||||
|
onClose={feature.close}
|
||||||
>
|
>
|
||||||
<div className="client-drawer-sheet client-subscription-sheet">
|
|
||||||
<header className="client-profiles-header">
|
<header className="client-profiles-header">
|
||||||
<h2>ПОДПИСКИ</h2>
|
<h2>ПОДПИСКИ</h2>
|
||||||
<button
|
|
||||||
ref={feature.closeRef}
|
|
||||||
className="client-drawer-close"
|
|
||||||
type="button"
|
|
||||||
aria-label="Закрыть подписки"
|
|
||||||
onClick={feature.close}
|
|
||||||
>×</button>
|
|
||||||
</header>
|
</header>
|
||||||
{statusSlot}
|
{statusSlot}
|
||||||
<div className="client-profile-list">
|
<div className="client-profile-list">
|
||||||
@@ -550,8 +546,7 @@ export function SubscriptionPanel({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={feature.showAdd}
|
onClick={feature.showAdd}
|
||||||
>+ Добавить подписку</button>}
|
>+ Добавить подписку</button>}
|
||||||
</div>
|
</Drawer>
|
||||||
</div>
|
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-11
@@ -40,7 +40,9 @@ p {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
font: 700 16px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-section-title);
|
||||||
|
letter-spacing: var(--type-section-title-tracking);
|
||||||
|
text-transform: var(--type-section-title-transform);
|
||||||
color: light-dark(oklch(0.42 0.01 145), oklch(0.76 0.01 145));
|
color: light-dark(oklch(0.42 0.01 145), oklch(0.76 0.01 145));
|
||||||
background: light-dark(oklch(0.965 0.006 145), oklch(0.18 0.012 145));
|
background: light-dark(oklch(0.965 0.006 145), oklch(0.18 0.012 145));
|
||||||
}
|
}
|
||||||
@@ -54,27 +56,31 @@ p {
|
|||||||
padding: 32px;
|
padding: 32px;
|
||||||
background: light-dark(oklch(0.965 0.006 145), oklch(0.18 0.012 145));
|
background: light-dark(oklch(0.965 0.006 145), oklch(0.18 0.012 145));
|
||||||
color: light-dark(oklch(0.24 0.014 145), oklch(0.93 0.008 145));
|
color: light-dark(oklch(0.24 0.014 145), oklch(0.93 0.008 145));
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font-family: var(--font-family-client);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-boot > span,
|
.app-boot > span,
|
||||||
.app-boot summary {
|
.app-boot summary {
|
||||||
color: light-dark(oklch(0.53 0.014 145), oklch(0.68 0.012 145));
|
color: light-dark(oklch(0.53 0.014 145), oklch(0.68 0.012 145));
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-boot h1 {
|
.app-boot h1 {
|
||||||
max-width: 22ch;
|
max-width: 22ch;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: clamp(22px, 5vw, 34px);
|
font: var(--type-drawer-title);
|
||||||
letter-spacing: -0.05em;
|
letter-spacing: var(--type-drawer-title-tracking);
|
||||||
|
text-transform: var(--type-drawer-title-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-boot p {
|
.app-boot p {
|
||||||
max-width: 58ch;
|
max-width: 58ch;
|
||||||
color: light-dark(oklch(0.53 0.014 145), oklch(0.68 0.012 145));
|
color: light-dark(oklch(0.53 0.014 145), oklch(0.68 0.012 145));
|
||||||
font-size: 12px;
|
font: var(--type-data);
|
||||||
line-height: 1.7;
|
letter-spacing: var(--type-data-tracking);
|
||||||
|
text-transform: var(--type-data-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-boot button,
|
.app-boot button,
|
||||||
@@ -106,8 +112,9 @@ p {
|
|||||||
display: block;
|
display: block;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin: 12px 0 0;
|
margin: 12px 0 0;
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
line-height: 1.6;
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +133,9 @@ p {
|
|||||||
box-shadow: 0 10px 32px oklch(0.08 0.015 145 / 0.14);
|
box-shadow: 0 10px 32px oklch(0.08 0.015 145 / 0.14);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font: 600 10px/1.3 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,5 +145,7 @@ p {
|
|||||||
|
|
||||||
.client-stale-banner button {
|
.client-stale-banner button {
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
.client-power-section p {
|
.client-power-section p {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-power-section {
|
.client-power-section {
|
||||||
@@ -47,9 +49,9 @@
|
|||||||
max-width: min(320px, calc(100vw - 42px));
|
max-width: min(320px, calc(100vw - 42px));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-size: 12px;
|
font: var(--type-data);
|
||||||
font-weight: 650;
|
letter-spacing: var(--type-data-tracking);
|
||||||
letter-spacing: -0.025em;
|
text-transform: var(--type-data-transform);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
transition: opacity 260ms ease, filter 360ms cubic-bezier(0.16, 1, 0.3, 1), transform 360ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: opacity 260ms ease, filter 360ms cubic-bezier(0.16, 1, 0.3, 1), transform 360ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
@@ -70,8 +72,9 @@
|
|||||||
min-height: 15px;
|
min-height: 15px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font-size: 8px;
|
font: var(--type-micro);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-micro-tracking);
|
||||||
|
text-transform: var(--type-micro-transform);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -248,8 +251,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-power-section h2 {
|
.client-power-section h2 {
|
||||||
font: 700 18px/1.3 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-state-title);
|
||||||
letter-spacing: -0.04em;
|
letter-spacing: var(--type-state-title-tracking);
|
||||||
|
text-transform: var(--type-state-title-transform);
|
||||||
animation: client-state-reveal 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
animation: client-state-reveal 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,16 +294,20 @@
|
|||||||
grid-area: 1 / 1;
|
grid-area: 1 / 1;
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-size: 14px;
|
font: var(--type-item-title);
|
||||||
font-variant-numeric: tabular-nums;
|
letter-spacing: var(--type-item-title-tracking);
|
||||||
letter-spacing: 0.04em;
|
text-transform: var(--type-item-title-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
filter: blur(5px);
|
filter: blur(5px);
|
||||||
transition: opacity 260ms ease, filter 420ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: opacity 260ms ease, filter 420ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-power-section.is-gateway .client-duration {
|
.client-power-section.is-gateway .client-duration {
|
||||||
font-size: 15px;
|
font: var(--type-item-title);
|
||||||
|
letter-spacing: var(--type-item-title-tracking);
|
||||||
|
text-transform: var(--type-item-title-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-duration.is-active {
|
.client-duration.is-active {
|
||||||
@@ -393,7 +401,6 @@
|
|||||||
|
|
||||||
.client-power-section p {
|
.client-power-section p {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
line-height: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes client-state-reveal {
|
@keyframes client-state-reveal {
|
||||||
@@ -432,10 +439,9 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
letter-spacing: 0.08em;
|
text-transform: var(--type-label-transform);
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-proxy-label > span {
|
.client-proxy-label > span {
|
||||||
@@ -477,8 +483,10 @@
|
|||||||
|
|
||||||
.client-proxy-address {
|
.client-proxy-address {
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-size: 12px;
|
font: var(--type-data);
|
||||||
font-weight: 500;
|
letter-spacing: var(--type-data-tracking);
|
||||||
|
text-transform: var(--type-data-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
text-shadow: 0 0 12px color-mix(in oklch, var(--client-accent) 12%, transparent);
|
text-shadow: 0 0 12px color-mix(in oklch, var(--client-accent) 12%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-devices-header h2 {
|
.client-devices-header h2 {
|
||||||
font-size: 24px;
|
font: var(--type-drawer-title);
|
||||||
|
letter-spacing: var(--type-drawer-title-tracking);
|
||||||
|
text-transform: var(--type-drawer-title-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-devices-header .client-instructions-intro {
|
.client-devices-header .client-instructions-intro {
|
||||||
font-size: 12px;
|
font: var(--type-data);
|
||||||
|
letter-spacing: var(--type-data-tracking);
|
||||||
|
text-transform: var(--type-data-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-devices-kicker {
|
.client-devices-kicker {
|
||||||
@@ -22,7 +26,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-devices-kicker > span:first-child {
|
.client-devices-kicker > span:first-child {
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-devices-refresh-wrap {
|
.client-devices-refresh-wrap {
|
||||||
@@ -49,7 +55,9 @@
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font: 700 9px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +75,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font: 700 10px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 220ms ease, filter 300ms ease;
|
transition: color 220ms ease, filter 300ms ease;
|
||||||
}
|
}
|
||||||
@@ -161,7 +171,6 @@
|
|||||||
|
|
||||||
.client-instructions-header .client-devices-refresh-wrap > .client-tooltip,
|
.client-instructions-header .client-devices-refresh-wrap > .client-tooltip,
|
||||||
.client-instructions-header .client-devices-sort-wrap > .client-tooltip {
|
.client-instructions-header .client-devices-sort-wrap > .client-tooltip {
|
||||||
text-transform: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes client-devices-refresh-progress {
|
@keyframes client-devices-refresh-progress {
|
||||||
@@ -173,8 +182,9 @@
|
|||||||
.client-devices-empty {
|
.client-devices-empty {
|
||||||
margin: 0 8px 20px;
|
margin: 0 8px 20px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
line-height: 1.6;
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-devices-source {
|
.client-devices-source {
|
||||||
@@ -226,10 +236,9 @@
|
|||||||
top: -17px;
|
top: -17px;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 8px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
letter-spacing: 0.1em;
|
text-transform: var(--type-label-transform);
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device:not(.is-group-start) > .client-device-group-heading {
|
.client-device:not(.is-group-start) > .client-device-group-heading {
|
||||||
@@ -271,8 +280,9 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
font-size: 14px;
|
font: var(--type-item-title);
|
||||||
letter-spacing: -0.03em;
|
letter-spacing: var(--type-item-title-tracking);
|
||||||
|
text-transform: var(--type-item-title-transform);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -298,8 +308,9 @@
|
|||||||
|
|
||||||
.client-device-alias-trigger {
|
.client-device-alias-trigger {
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
font: 700 14px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-item-title);
|
||||||
letter-spacing: -0.03em;
|
letter-spacing: var(--type-item-title-tracking);
|
||||||
|
text-transform: var(--type-item-title-transform);
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,8 +324,9 @@
|
|||||||
outline: 0;
|
outline: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
caret-color: var(--client-accent);
|
caret-color: var(--client-accent);
|
||||||
font: 700 14px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-item-title);
|
||||||
letter-spacing: -0.03em;
|
letter-spacing: var(--type-item-title-tracking);
|
||||||
|
text-transform: var(--type-item-title-transform);
|
||||||
animation: client-device-alias-edit-in 360ms cubic-bezier(0.16, 1, 0.3, 1);
|
animation: client-device-alias-edit-in 360ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,12 +346,17 @@
|
|||||||
.client-device-name-separator {
|
.client-device-name-separator {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-ip {
|
.client-device-ip {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
font: 600 12px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-data);
|
||||||
|
letter-spacing: var(--type-data-tracking);
|
||||||
|
text-transform: var(--type-data-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
cursor: copy;
|
cursor: copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +495,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 8.5px;
|
font: var(--type-micro);
|
||||||
|
letter-spacing: var(--type-micro-tracking);
|
||||||
|
text-transform: var(--type-micro-transform);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@@ -493,7 +512,7 @@
|
|||||||
|
|
||||||
.client-device-last-seen.is-online {
|
.client-device-last-seen.is-online {
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font-weight: 700;
|
font-weight: var(--font-weight-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-pin-wrap.client-tooltip-anchor > .client-tooltip {
|
.client-device-pin-wrap.client-tooltip-anchor > .client-tooltip {
|
||||||
@@ -564,7 +583,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-total,
|
.client-device-traffic-total,
|
||||||
@@ -624,15 +643,17 @@
|
|||||||
|
|
||||||
.client-device-traffic b {
|
.client-device-traffic b {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 8px;
|
font: var(--type-label);
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: var(--type-label-tracking);
|
||||||
text-transform: uppercase;
|
text-transform: var(--type-label-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic strong {
|
.client-device-traffic strong {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
font-weight: 600;
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -747,7 +768,9 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
color: color-mix(in oklch, var(--client-text) 62%, var(--client-muted));
|
color: color-mix(in oklch, var(--client-text) 62%, var(--client-muted));
|
||||||
font: 700 7.5px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-micro);
|
||||||
|
letter-spacing: var(--type-micro-tracking);
|
||||||
|
text-transform: var(--type-micro-transform);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -862,8 +885,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
color: color-mix(in oklch, var(--client-text) 46%, var(--client-muted));
|
color: color-mix(in oklch, var(--client-text) 46%, var(--client-muted));
|
||||||
font-size: 8.5px;
|
font: var(--type-micro);
|
||||||
line-height: 10px;
|
letter-spacing: var(--type-micro-tracking);
|
||||||
|
text-transform: var(--type-micro-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-point-tooltip {
|
.client-device-traffic-point-tooltip {
|
||||||
@@ -881,8 +905,10 @@
|
|||||||
inset 0 0 0 1px color-mix(in oklch, var(--client-border) 55%, transparent),
|
inset 0 0 0 1px color-mix(in oklch, var(--client-border) 55%, transparent),
|
||||||
0 8px 24px oklch(0.08 0.015 145 / 0.22);
|
0 8px 24px oklch(0.08 0.015 145 / 0.22);
|
||||||
color: oklch(0.92 0.008 145);
|
color: oklch(0.92 0.008 145);
|
||||||
font: 600 9px/1.25 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-label);
|
||||||
font-variant-numeric: tabular-nums;
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: client-device-traffic-tooltip-in 140ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: client-device-traffic-tooltip-in 140ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
@@ -890,12 +916,18 @@
|
|||||||
|
|
||||||
.client-device-traffic-point-tooltip time {
|
.client-device-traffic-point-tooltip time {
|
||||||
color: oklch(0.68 0.012 145);
|
color: oklch(0.68 0.012 145);
|
||||||
font-size: 8px;
|
font: var(--type-micro);
|
||||||
|
letter-spacing: var(--type-micro-tracking);
|
||||||
|
text-transform: var(--type-micro-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-point-tooltip strong {
|
.client-device-traffic-point-tooltip strong {
|
||||||
color: oklch(0.98 0.006 145);
|
color: oklch(0.98 0.006 145);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-point-tooltip .is-proxy {
|
.client-device-traffic-point-tooltip .is-proxy {
|
||||||
@@ -1027,7 +1059,6 @@
|
|||||||
.client-device-deprioritize-wrap.client-tooltip-anchor > .client-tooltip {
|
.client-device-deprioritize-wrap.client-tooltip-anchor > .client-tooltip {
|
||||||
right: 0;
|
right: 0;
|
||||||
left: auto;
|
left: auto;
|
||||||
text-transform: none;
|
|
||||||
transform: translate(0, 2px);
|
transform: translate(0, 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1069,8 +1100,9 @@
|
|||||||
|
|
||||||
.client-devices-error button {
|
.client-devices-error button {
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-devices button:focus-visible {
|
.client-devices button:focus-visible {
|
||||||
@@ -1101,10 +1133,9 @@
|
|||||||
|
|
||||||
.client-gateway-traffic-total > small {
|
.client-gateway-traffic-total > small {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
letter-spacing: 0.08em;
|
text-transform: var(--type-label-transform);
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-traffic-heading {
|
.client-gateway-traffic-heading {
|
||||||
@@ -1128,9 +1159,10 @@
|
|||||||
|
|
||||||
.client-gateway-traffic-total > strong {
|
.client-gateway-traffic-total > strong {
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-size: 15px;
|
font: var(--type-item-title);
|
||||||
font-weight: 600;
|
letter-spacing: var(--type-item-title-tracking);
|
||||||
font-variant-numeric: tabular-nums;
|
text-transform: var(--type-item-title-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-traffic-speed {
|
.client-gateway-traffic-speed {
|
||||||
@@ -1138,8 +1170,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
font-variant-numeric: tabular-nums;
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1166,8 +1200,9 @@
|
|||||||
.client-gateway-traffic-freshness {
|
.client-gateway-traffic-freshness {
|
||||||
min-height: 26px;
|
min-height: 26px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 8.5px;
|
font: var(--type-micro);
|
||||||
line-height: 1.5;
|
letter-spacing: var(--type-micro-tracking);
|
||||||
|
text-transform: var(--type-micro-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-traffic-freshness.is-stale {
|
.client-gateway-traffic-freshness.is-stale {
|
||||||
@@ -1187,7 +1222,10 @@
|
|||||||
.client-gateway-traffic-speed {
|
.client-gateway-traffic-speed {
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-traffic-chart,
|
.client-gateway-traffic-chart,
|
||||||
|
|||||||
@@ -75,7 +75,6 @@
|
|||||||
.client-diagnostics-refresh-wrap > .client-tooltip {
|
.client-diagnostics-refresh-wrap > .client-tooltip {
|
||||||
right: 0;
|
right: 0;
|
||||||
left: auto;
|
left: auto;
|
||||||
text-transform: none;
|
|
||||||
transform: translate(0, 2px);
|
transform: translate(0, 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +95,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
color: oklch(0.68 0.15 28);
|
color: oklch(0.68 0.15 28);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
line-height: 1.5;
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-diagnostics-error button {
|
.client-diagnostics-error button {
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-weight: 700;
|
font-weight: var(--font-weight-bold);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,10 +122,9 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
letter-spacing: 0.08em;
|
text-transform: var(--type-label-transform);
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-diagnostics-section-title button {
|
.client-diagnostics-section-title button {
|
||||||
@@ -133,7 +132,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font: 700 9px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,10 +174,9 @@
|
|||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
padding-bottom: 7px;
|
padding-bottom: 7px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 8px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
letter-spacing: 0.05em;
|
text-transform: var(--type-label-transform);
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-diagnostics-table tbody tr {
|
.client-diagnostics-table tbody tr {
|
||||||
@@ -219,8 +219,9 @@
|
|||||||
|
|
||||||
.client-diagnostics-table tbody th {
|
.client-diagnostics-table tbody th {
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-weight: 600;
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
transition: color 420ms ease, text-shadow 520ms ease;
|
transition: color 420ms ease, text-shadow 520ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,10 +240,9 @@
|
|||||||
.client-diagnostics-service-header {
|
.client-diagnostics-service-header {
|
||||||
padding: 2px 0 7px;
|
padding: 2px 0 7px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 8px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
letter-spacing: 0.05em;
|
text-transform: var(--type-label-transform);
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-diagnostics-service-header > span,
|
.client-diagnostics-service-header > span,
|
||||||
@@ -257,12 +257,12 @@
|
|||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
min-height: 37px;
|
min-height: 37px;
|
||||||
border-top: 1px solid color-mix(in oklch, var(--client-border) 48%, transparent);
|
border-top: 1px solid color-mix(in oklch, var(--client-border) 48%, transparent);
|
||||||
animation: client-local-rule-enter 560ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: client-row-enter 560ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-diagnostics-service-row.is-removing {
|
.client-diagnostics-service-row.is-removing {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: client-local-rule-leave 820ms cubic-bezier(0.7, 0, 0.84, 0) both;
|
animation: client-row-leave 820ms cubic-bezier(0.7, 0, 0.84, 0) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-diagnostics-service-row.is-running .client-diagnostics-service-name {
|
.client-diagnostics-service-row.is-running .client-diagnostics-service-name {
|
||||||
@@ -273,8 +273,9 @@
|
|||||||
.client-diagnostics-service-name {
|
.client-diagnostics-service-name {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-weight: 600;
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
transition: color 420ms ease, text-shadow 520ms ease;
|
transition: color 420ms ease, text-shadow 520ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +285,10 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font: 600 9px/1.4 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-diagnostics-status.is-good {
|
.client-diagnostics-status.is-good {
|
||||||
@@ -338,7 +342,9 @@
|
|||||||
outline: 0;
|
outline: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font: 500 9px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
box-shadow: 0 1px 0 transparent;
|
box-shadow: 0 1px 0 transparent;
|
||||||
transition: box-shadow 300ms ease, text-shadow 300ms ease;
|
transition: box-shadow 300ms ease, text-shadow 300ms ease;
|
||||||
}
|
}
|
||||||
@@ -369,7 +375,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font: 700 8px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: filter 260ms ease, text-shadow 300ms ease;
|
transition: filter 260ms ease, text-shadow 300ms ease;
|
||||||
}
|
}
|
||||||
@@ -384,7 +392,9 @@
|
|||||||
.client-diagnostics-services-empty {
|
.client-diagnostics-services-empty {
|
||||||
padding: 14px 8px 4px;
|
padding: 14px 8px 4px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-diagnostics-add-slot {
|
.client-diagnostics-add-slot {
|
||||||
@@ -420,7 +430,7 @@
|
|||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-diagnostics-service-draft > .client-local-rule-delete {
|
.client-diagnostics-service-draft > .client-row-delete {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1 / 3;
|
grid-row: 1 / 3;
|
||||||
width: 44px;
|
width: 44px;
|
||||||
|
|||||||
@@ -116,14 +116,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-instruction-summary strong {
|
.client-instruction-summary strong {
|
||||||
font-size: 14px;
|
font: var(--type-item-title);
|
||||||
|
letter-spacing: var(--type-item-title-tracking);
|
||||||
|
text-transform: var(--type-item-title-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instruction-summary small {
|
.client-instruction-summary small {
|
||||||
max-width: 54ch;
|
max-width: 54ch;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
line-height: 1.55;
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instruction-reveal {
|
.client-instruction-reveal {
|
||||||
@@ -147,8 +150,9 @@
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 0 22px 24px;
|
padding: 0 22px 24px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
line-height: 1.7;
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instruction-body p,
|
.client-instruction-body p,
|
||||||
@@ -164,7 +168,7 @@
|
|||||||
|
|
||||||
.client-instruction-body li::marker {
|
.client-instruction-body li::marker {
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instruction-body code {
|
.client-instruction-body code {
|
||||||
@@ -174,7 +178,9 @@
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: color-mix(in oklch, var(--client-control) 84%, transparent);
|
background: color-mix(in oklch, var(--client-control) 84%, transparent);
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font: 500 11px/1.5 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-body);
|
||||||
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +198,6 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
line-height: 1.55;
|
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +219,9 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -229,7 +236,7 @@
|
|||||||
.client-instruction-body a {
|
.client-instruction-body a {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font-weight: 700;
|
font-weight: var(--font-weight-bold);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,24 +12,25 @@
|
|||||||
.client-local-rules-header > span,
|
.client-local-rules-header > span,
|
||||||
.client-local-rules-group > span {
|
.client-local-rules-group > span {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
letter-spacing: 0.08em;
|
text-transform: var(--type-label-transform);
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rules-header h2 {
|
.client-local-rules-header h2 {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
font-size: 22px;
|
font: var(--type-drawer-title);
|
||||||
letter-spacing: -0.045em;
|
letter-spacing: var(--type-drawer-title-tracking);
|
||||||
|
text-transform: var(--type-drawer-title-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rules-header p {
|
.client-local-rules-header p {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
max-width: 46ch;
|
max-width: 46ch;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
line-height: 1.7;
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rules-save {
|
.client-local-rules-save {
|
||||||
@@ -38,8 +39,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 200ms ease, filter 300ms ease, opacity 220ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: color 200ms ease, filter 300ms ease, opacity 220ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
@@ -86,7 +88,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 7px 0;
|
padding: 7px 0;
|
||||||
animation: client-local-rule-enter 560ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: client-row-enter 560ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
transition: opacity 260ms ease, filter 360ms ease;
|
transition: opacity 260ms ease, filter 360ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,11 +98,10 @@
|
|||||||
|
|
||||||
.client-local-rule-status {
|
.client-local-rule-status {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
letter-spacing: 0.07em;
|
text-transform: var(--type-label-transform);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
text-transform: uppercase;
|
|
||||||
transition: color 280ms ease, opacity 280ms ease, filter 360ms ease;
|
transition: color 280ms ease, opacity 280ms ease, filter 360ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@
|
|||||||
|
|
||||||
.client-local-rule.is-removing {
|
.client-local-rule.is-removing {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: client-local-rule-leave 820ms cubic-bezier(0.7, 0, 0.84, 0) both;
|
animation: client-row-leave 820ms cubic-bezier(0.7, 0, 0.84, 0) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-enabled {
|
.client-local-rule-enabled {
|
||||||
@@ -196,7 +197,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font: 600 9px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 220ms ease, text-shadow 300ms ease;
|
transition: color 220ms ease, text-shadow 300ms ease;
|
||||||
@@ -255,7 +258,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font: 600 9px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -295,7 +300,9 @@
|
|||||||
outline: 0;
|
outline: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font: 500 10px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
box-shadow: 0 1px 0 transparent;
|
box-shadow: 0 1px 0 transparent;
|
||||||
transition: color 220ms ease, box-shadow 300ms ease, text-shadow 300ms ease;
|
transition: color 220ms ease, box-shadow 300ms ease, text-shadow 300ms ease;
|
||||||
}
|
}
|
||||||
@@ -312,15 +319,17 @@
|
|||||||
.client-local-rules-empty {
|
.client-local-rules-empty {
|
||||||
padding: 14px 4px 4px;
|
padding: 14px 4px 4px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
line-height: 1.6;
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rules-note {
|
.client-local-rules-note {
|
||||||
max-width: 48ch;
|
max-width: 48ch;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
line-height: 1.65;
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-inline-error.is-routing {
|
.client-inline-error.is-routing {
|
||||||
@@ -340,8 +349,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 200ms ease, filter 300ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: color 200ms ease, filter 300ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
@@ -369,9 +379,9 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin: -10px 0 -8px;
|
margin: -10px 0 -8px;
|
||||||
color: var(--client-warning, oklch(0.72 0.12 72));
|
color: var(--client-warning, oklch(0.72 0.12 72));
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
letter-spacing: 0.06em;
|
text-transform: var(--type-label-transform);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
filter: blur(5px);
|
filter: blur(5px);
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
|
|||||||
@@ -6,11 +6,10 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
letter-spacing: 0.08em;
|
text-transform: var(--type-label-transform);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-transform: uppercase;
|
|
||||||
animation: client-state-reveal 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: client-state-reveal 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,14 +92,17 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
line-height: 1.4;
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server small {
|
.client-server small {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,9 +110,10 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-variant-numeric: tabular-nums;
|
letter-spacing: var(--type-label-tracking);
|
||||||
line-height: 1.2;
|
text-transform: var(--type-label-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,9 +177,9 @@
|
|||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font: 700 9px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-label);
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: var(--type-label-tracking);
|
||||||
text-transform: uppercase;
|
text-transform: var(--type-label-transform);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +200,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font: 700 9px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 220ms ease, text-shadow 320ms ease;
|
transition: color 220ms ease, text-shadow 320ms ease;
|
||||||
}
|
}
|
||||||
@@ -357,7 +362,9 @@
|
|||||||
outline: 0;
|
outline: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font: 500 11px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-body);
|
||||||
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server-tools input:focus {
|
.client-server-tools input:focus {
|
||||||
@@ -379,7 +386,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font: 700 9px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,12 +431,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-server-auto strong {
|
.client-server-auto strong {
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server-auto small {
|
.client-server-auto small {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server-scroll {
|
.client-server-scroll {
|
||||||
@@ -471,11 +484,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server-group-toggle small {
|
.client-server-group-toggle small {
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-servers.is-scalable .client-server-grid {
|
.client-servers.is-scalable .client-server-grid {
|
||||||
@@ -508,7 +522,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-border);
|
color: var(--client-border);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
@@ -522,7 +538,9 @@
|
|||||||
.client-server-empty {
|
.client-server-empty {
|
||||||
padding: 24px 4px;
|
padding: 24px 4px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,7 +554,9 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server-more:disabled {
|
.client-server-more:disabled {
|
||||||
@@ -592,9 +612,9 @@
|
|||||||
|
|
||||||
.client-profile-body .client-server-check-label {
|
.client-profile-body .client-server-check-label {
|
||||||
display: inline;
|
display: inline;
|
||||||
font: 600 9px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-label);
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: var(--type-label-tracking);
|
||||||
text-transform: uppercase;
|
text-transform: var(--type-label-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-body .client-server-mode-toggle {
|
.client-profile-body .client-server-mode-toggle {
|
||||||
@@ -705,12 +725,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-body .client-server strong {
|
.client-profile-body .client-server strong {
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-body .client-server small {
|
.client-profile-body .client-server small {
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
font-size: 8px;
|
font: var(--type-micro);
|
||||||
|
letter-spacing: var(--type-micro-tracking);
|
||||||
|
text-transform: var(--type-micro-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-body .client-server-meta {
|
.client-profile-body .client-server-meta {
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
.client-inline-error.is-subscription small {
|
.client-inline-error.is-subscription small {
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
font-size: 8px;
|
font: var(--type-micro);
|
||||||
font-weight: 500;
|
letter-spacing: var(--type-micro-tracking);
|
||||||
letter-spacing: 0.04em;
|
text-transform: var(--type-micro-transform);
|
||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
animation: client-subscription-code-in 560ms 90ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: client-subscription-code-in 560ms 90ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
}
|
}
|
||||||
@@ -56,8 +56,9 @@
|
|||||||
.client-profiles-header h2 {
|
.client-profiles-header h2 {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font: var(--type-drawer-title);
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: var(--type-drawer-title-tracking);
|
||||||
|
text-transform: var(--type-drawer-title-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-list {
|
.client-profile-list {
|
||||||
@@ -95,7 +96,7 @@
|
|||||||
|
|
||||||
.client-profile-chevron {
|
.client-profile-chevron {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 20px;
|
font-size: var(--font-size-icon-chevron);
|
||||||
transform: rotate(0);
|
transform: rotate(0);
|
||||||
transition: transform 320ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: transform 320ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
@@ -126,28 +127,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-title strong {
|
.client-profile-title strong {
|
||||||
font-size: 12px;
|
font: var(--type-data);
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: var(--type-data-tracking);
|
||||||
|
text-transform: var(--type-data-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-title small {
|
.client-profile-title small {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-title em {
|
.client-profile-title em {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font-size: 7px;
|
font: var(--type-micro);
|
||||||
|
letter-spacing: var(--type-micro-tracking);
|
||||||
|
text-transform: var(--type-micro-transform);
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-usage {
|
.client-profile-usage {
|
||||||
max-width: 126px;
|
max-width: 126px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 8px;
|
font: var(--type-micro);
|
||||||
|
letter-spacing: var(--type-micro-tracking);
|
||||||
|
text-transform: var(--type-micro-transform);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -293,14 +300,18 @@
|
|||||||
.client-profile-selected-server strong {
|
.client-profile-selected-server strong {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-selected-server small {
|
.client-profile-selected-server small {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 8px;
|
font: var(--type-micro);
|
||||||
|
letter-spacing: var(--type-micro-tracking);
|
||||||
|
text-transform: var(--type-micro-transform);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,9 +332,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font: 600 9px/1 inherit;
|
font: var(--type-label);
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: var(--type-label-tracking);
|
||||||
text-transform: uppercase;
|
text-transform: var(--type-label-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,8 +388,9 @@
|
|||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 8px;
|
font: var(--type-micro);
|
||||||
line-height: 1.45;
|
letter-spacing: var(--type-micro-tracking);
|
||||||
|
text-transform: var(--type-micro-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-local-status {
|
.client-profile-local-status {
|
||||||
@@ -403,7 +415,9 @@
|
|||||||
outline: 0;
|
outline: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font: 500 10px/1 inherit;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-add input:focus {
|
.client-profile-add input:focus {
|
||||||
@@ -426,7 +440,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font: 600 9px/1 inherit;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,13 +467,15 @@
|
|||||||
|
|
||||||
.client-first-run-copy span {
|
.client-first-run-copy span {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: var(--type-label-tracking);
|
||||||
text-transform: uppercase;
|
text-transform: var(--type-label-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-first-run-copy strong {
|
.client-first-run-copy strong {
|
||||||
font-size: 16px;
|
font: var(--type-section-title);
|
||||||
|
letter-spacing: var(--type-section-title-tracking);
|
||||||
|
text-transform: var(--type-section-title-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
@media (max-width: 520px) {
|
||||||
@@ -471,8 +489,7 @@
|
|||||||
|
|
||||||
.client-profile-disclosure,
|
.client-profile-disclosure,
|
||||||
.client-profile-refresh,
|
.client-profile-refresh,
|
||||||
.client-profile-delete,
|
.client-profile-delete {
|
||||||
.client-profiles-header > button:not(.client-drawer-close) {
|
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-35
@@ -22,7 +22,7 @@
|
|||||||
width: min(100%, 1100px);
|
width: min(100%, 1100px);
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font-family: var(--font-family-client);
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +35,10 @@
|
|||||||
justify-items: end;
|
justify-items: end;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font: 650 12px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-data);
|
||||||
font-variant-numeric: tabular-nums;
|
letter-spacing: var(--type-data-tracking);
|
||||||
|
text-transform: var(--type-data-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
}
|
}
|
||||||
|
|
||||||
.harbor-version {
|
.harbor-version {
|
||||||
@@ -61,13 +63,17 @@
|
|||||||
.harbor-version-code {
|
.harbor-version-code {
|
||||||
width: 1.3ch;
|
width: 1.3ch;
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.harbor-version-number {
|
.harbor-version-number {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
font-variant-numeric: var(--numeric-tabular);
|
||||||
}
|
}
|
||||||
|
|
||||||
.harbor-version-part {
|
.harbor-version-part {
|
||||||
@@ -102,9 +108,9 @@
|
|||||||
background: oklch(0.14 0.012 145);
|
background: oklch(0.14 0.012 145);
|
||||||
box-shadow: 0 9px 30px oklch(0.08 0.015 145 / 0.16);
|
box-shadow: 0 9px 30px oklch(0.08 0.015 145 / 0.16);
|
||||||
color: oklch(0.88 0.012 145);
|
color: oklch(0.88 0.012 145);
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
font-weight: 500;
|
letter-spacing: var(--type-body-tracking);
|
||||||
line-height: 1.5;
|
text-transform: var(--type-body-transform);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@@ -116,12 +122,16 @@
|
|||||||
|
|
||||||
.harbor-version-tooltip strong {
|
.harbor-version-tooltip strong {
|
||||||
color: oklch(0.96 0.008 145);
|
color: oklch(0.96 0.008 145);
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.harbor-version-tooltip small {
|
.harbor-version-tooltip small {
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.harbor-version-tooltip .is-warning {
|
.harbor-version-tooltip .is-warning {
|
||||||
@@ -143,8 +153,8 @@
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
color: var(--harbor-word);
|
color: var(--harbor-word);
|
||||||
font-size: 13px;
|
font-size: var(--font-size-brand);
|
||||||
letter-spacing: -0.035em;
|
letter-spacing: var(--tracking-title);
|
||||||
transform: translate(-50%, calc(-50% - 25vh)) scale(3);
|
transform: translate(-50%, calc(-50% - 25vh)) scale(3);
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
transition: transform 1800ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: transform 1800ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
@@ -264,7 +274,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.harbor-brand strong {
|
.harbor-brand strong {
|
||||||
font-weight: 800;
|
font-weight: var(--font-weight-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.harbor-brand-name {
|
.harbor-brand-name {
|
||||||
@@ -276,7 +286,7 @@
|
|||||||
.harbor-brand em {
|
.harbor-brand em {
|
||||||
color: var(--harbor-connect);
|
color: var(--harbor-connect);
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: var(--font-weight-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.harbor-brand.is-gateway em {
|
.harbor-brand.is-gateway em {
|
||||||
@@ -464,9 +474,9 @@
|
|||||||
background: oklch(0.14 0.012 145);
|
background: oklch(0.14 0.012 145);
|
||||||
box-shadow: 0 5px 18px oklch(0.08 0.015 145 / 0.1);
|
box-shadow: 0 5px 18px oklch(0.08 0.015 145 / 0.1);
|
||||||
color: oklch(0.88 0.012 145);
|
color: oklch(0.88 0.012 145);
|
||||||
font-size: 4px;
|
font-size: var(--font-size-brand-tooltip);
|
||||||
line-height: 1.5;
|
line-height: var(--line-height-micro);
|
||||||
letter-spacing: 0;
|
letter-spacing: var(--tracking-normal);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@@ -478,7 +488,7 @@
|
|||||||
|
|
||||||
.harbor-mode-tooltip strong {
|
.harbor-mode-tooltip strong {
|
||||||
color: oklch(0.96 0.008 145);
|
color: oklch(0.96 0.008 145);
|
||||||
font-size: 4.2px;
|
font-size: var(--font-size-brand-tooltip-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.harbor-brand-control:hover .harbor-mode-tooltip,
|
.harbor-brand-control:hover .harbor-mode-tooltip,
|
||||||
@@ -696,7 +706,6 @@
|
|||||||
|
|
||||||
.client-copy-button {
|
.client-copy-button {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
font-size: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server-filters button,
|
.client-server-filters button,
|
||||||
@@ -726,18 +735,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-enabled,
|
.client-local-rule-enabled,
|
||||||
.client-local-rule-delete {
|
.client-row-delete {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rules-header > span,
|
|
||||||
.client-local-rules-group > span,
|
|
||||||
.client-instructions-header span,
|
|
||||||
.client-instruction-summary > span {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-instructions-sheet {
|
.client-instructions-sheet {
|
||||||
padding: 40px 58px 60px 18px;
|
padding: 40px 58px 60px 18px;
|
||||||
}
|
}
|
||||||
@@ -746,10 +748,6 @@
|
|||||||
padding: 40px 58px 60px 18px;
|
padding: 40px 58px 60px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rules-header h2 {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-local-rule {
|
.client-local-rule {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
@@ -764,7 +762,7 @@
|
|||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-delete {
|
.client-row-delete {
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
}
|
}
|
||||||
@@ -774,10 +772,6 @@
|
|||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instructions-header h2 {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-instruction-block:nth-child(n) {
|
.client-instruction-block:nth-child(n) {
|
||||||
margin-inline: 0;
|
margin-inline: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instructions-toggle,
|
.client-rail-action {
|
||||||
.client-local-rules-toggle {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 42px;
|
width: 42px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
@@ -22,8 +21,7 @@
|
|||||||
transition: color 240ms ease, transform 440ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: color 240ms ease, transform 440ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instructions-toggle svg,
|
.client-rail-action svg {
|
||||||
.client-local-rules-toggle svg {
|
|
||||||
width: 23px;
|
width: 23px;
|
||||||
height: 23px;
|
height: 23px;
|
||||||
fill: none;
|
fill: none;
|
||||||
@@ -35,14 +33,14 @@
|
|||||||
transition: color 240ms ease, filter 360ms ease, transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: color 240ms ease, filter 360ms ease, transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instructions-toggle span,
|
.client-rail-action > span {
|
||||||
.client-local-rules-toggle span {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 48px;
|
right: 48px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font: 700 10px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-control);
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
filter: blur(5px);
|
filter: blur(5px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -50,30 +48,23 @@
|
|||||||
transition: opacity 350ms ease, filter 350ms ease, transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: opacity 350ms ease, filter 350ms ease, transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instructions-toggle:hover,
|
.client-rail-action:hover,
|
||||||
.client-instructions-toggle:focus-visible,
|
.client-rail-action:focus-visible {
|
||||||
.client-local-rules-toggle:hover,
|
|
||||||
.client-local-rules-toggle:focus-visible {
|
|
||||||
outline: 0;
|
outline: 0;
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
transform: translateX(-3px);
|
transform: translateX(-3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instructions-toggle:hover span,
|
.client-rail-action:hover > span,
|
||||||
.client-instructions-toggle:focus-visible span,
|
.client-rail-action:focus-visible > span {
|
||||||
.client-local-rules-toggle:hover span,
|
|
||||||
.client-local-rules-toggle:focus-visible span {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
filter: blur(0);
|
filter: blur(0);
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instructions-toggle:hover svg,
|
.client-rail-action:hover svg,
|
||||||
.client-instructions-toggle:focus-visible svg,
|
.client-rail-action:focus-visible svg,
|
||||||
.client-local-rules-toggle:hover svg,
|
.client-rail-action.is-open svg {
|
||||||
.client-local-rules-toggle:focus-visible svg,
|
|
||||||
.client-instructions-toggle.is-open svg,
|
|
||||||
.client-local-rules-toggle.is-open svg {
|
|
||||||
filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-accent) 52%, transparent));
|
filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-accent) 52%, transparent));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +162,7 @@
|
|||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instructions-toggle.is-open,
|
.client-rail-action.is-open {
|
||||||
.client-local-rules-toggle.is-open {
|
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,8 +174,7 @@
|
|||||||
filter: drop-shadow(0 0 7px oklch(0.68 0.14 72 / 0.42));
|
filter: drop-shadow(0 0 7px oklch(0.68 0.14 72 / 0.42));
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instructions-toggle.is-open span,
|
.client-rail-action.is-open > span {
|
||||||
.client-local-rules-toggle.is-open span {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
@@ -215,7 +204,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
padding: 54px 72px 72px 34px;
|
padding: 54px 72px 72px 34px;
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font-family: var(--font-family-client);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-delete-strike {
|
.client-delete-strike {
|
||||||
@@ -262,19 +251,19 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-delete {
|
.client-row-delete {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 17px;
|
font-size: var(--font-size-icon-delete);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 180ms ease, filter 260ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: color 180ms ease, filter 260ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-delete:hover {
|
.client-row-delete:hover {
|
||||||
color: oklch(0.68 0.15 28);
|
color: oklch(0.68 0.15 28);
|
||||||
filter: drop-shadow(0 0 7px oklch(0.68 0.15 28 / 0.42));
|
filter: drop-shadow(0 0 7px oklch(0.68 0.15 28 / 0.42));
|
||||||
transform: rotate(8deg) scale(1.1);
|
transform: rotate(8deg) scale(1.1);
|
||||||
@@ -282,8 +271,8 @@
|
|||||||
|
|
||||||
.client-local-rule-enabled:focus-visible,
|
.client-local-rule-enabled:focus-visible,
|
||||||
.client-rule-type-trigger:focus-visible,
|
.client-rule-type-trigger:focus-visible,
|
||||||
.client-local-rule-delete:focus-visible,
|
.client-row-delete:focus-visible,
|
||||||
.client-local-rule-add:focus-visible,
|
.client-row-add:focus-visible,
|
||||||
.client-local-rules-save:focus-visible,
|
.client-local-rules-save:focus-visible,
|
||||||
.client-local-rules-actions button:focus-visible {
|
.client-local-rules-actions button:focus-visible {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
@@ -291,56 +280,59 @@
|
|||||||
text-shadow: 0 0 10px color-mix(in oklch, var(--client-accent) 48%, transparent);
|
text-shadow: 0 0 10px color-mix(in oklch, var(--client-accent) 48%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-add {
|
.client-row-add {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding: 7px 0;
|
padding: 7px 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 220ms ease, color 220ms ease, text-shadow 300ms ease;
|
transition: opacity 220ms ease, color 220ms ease, text-shadow 300ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-add:hover {
|
.client-row-add:hover {
|
||||||
text-shadow: 0 0 10px color-mix(in oklch, var(--client-accent) 55%, transparent);
|
text-shadow: 0 0 10px color-mix(in oklch, var(--client-accent) 55%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-add:disabled {
|
.client-row-add:disabled {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-add-slot {
|
.client-row-add-slot {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-add-slot > span {
|
.client-row-add-slot > span {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 8px;
|
font: var(--type-micro);
|
||||||
|
letter-spacing: var(--type-micro-tracking);
|
||||||
|
text-transform: var(--type-micro-transform);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
filter: blur(5px);
|
filter: blur(5px);
|
||||||
transform: translateX(-6px);
|
transform: translateX(-6px);
|
||||||
transition: opacity 220ms ease, filter 320ms ease, transform 380ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: opacity 220ms ease, filter 320ms ease, transform 380ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-add-slot > span.is-visible {
|
.client-row-add-slot > span.is-visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
filter: blur(0);
|
filter: blur(0);
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes client-local-rule-enter {
|
@keyframes client-row-enter {
|
||||||
from { opacity: 0; filter: blur(8px); transform: translateY(-10px) scale(0.985); }
|
from { opacity: 0; filter: blur(8px); transform: translateY(-10px) scale(0.985); }
|
||||||
to { opacity: 1; filter: blur(0); transform: translateY(0) scale(1); }
|
to { opacity: 1; filter: blur(0); transform: translateY(0) scale(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes client-local-rule-leave {
|
@keyframes client-row-leave {
|
||||||
0%, 52% { opacity: 1; filter: blur(0); transform: translateX(0); }
|
0%, 52% { opacity: 1; filter: blur(0); transform: translateX(0); }
|
||||||
100% { opacity: 0; filter: blur(8px); transform: translateX(28px); }
|
100% { opacity: 0; filter: blur(8px); transform: translateX(28px); }
|
||||||
}
|
}
|
||||||
@@ -373,7 +365,7 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font: 400 24px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-icon-close);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 220ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: color 220ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
@@ -391,16 +383,16 @@
|
|||||||
.client-instructions-header span,
|
.client-instructions-header span,
|
||||||
.client-instruction-summary > span {
|
.client-instruction-summary > span {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
letter-spacing: 0.08em;
|
text-transform: var(--type-label-transform);
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instructions-header h2 {
|
.client-instructions-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 22px;
|
font: var(--type-drawer-title);
|
||||||
letter-spacing: -0.045em;
|
letter-spacing: var(--type-drawer-title-tracking);
|
||||||
|
text-transform: var(--type-drawer-title-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instructions-intro {
|
.client-instructions-intro {
|
||||||
@@ -408,8 +400,9 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
line-height: 1.7;
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-instructions-intro p {
|
.client-instructions-intro p {
|
||||||
@@ -441,8 +434,9 @@
|
|||||||
background: oklch(0.14 0.012 145);
|
background: oklch(0.14 0.012 145);
|
||||||
box-shadow: 0 8px 26px oklch(0.08 0.015 145 / 0.16);
|
box-shadow: 0 8px 26px oklch(0.08 0.015 145 / 0.16);
|
||||||
color: oklch(0.94 0.008 145);
|
color: oklch(0.94 0.008 145);
|
||||||
font: 600 10px/1.35 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-tooltip);
|
||||||
letter-spacing: 0;
|
letter-spacing: var(--type-tooltip-tracking);
|
||||||
|
text-transform: var(--type-tooltip-transform);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -527,7 +521,7 @@
|
|||||||
background: color-mix(in oklch, var(--client-bg) 82%, transparent);
|
background: color-mix(in oklch, var(--client-bg) 82%, transparent);
|
||||||
box-shadow: 0 0 86px color-mix(in oklch, var(--client-bg) 86%, transparent);
|
box-shadow: 0 0 86px color-mix(in oklch, var(--client-bg) 86%, transparent);
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font-family: var(--font-family-client);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
filter: blur(18px);
|
filter: blur(18px);
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
@@ -578,23 +572,24 @@
|
|||||||
|
|
||||||
.client-confirmation-kicker {
|
.client-confirmation-kicker {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
letter-spacing: 0.08em;
|
text-transform: var(--type-label-transform);
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-confirmation-dialog h2 {
|
.client-confirmation-dialog h2 {
|
||||||
color: oklch(0.68 0.15 28);
|
color: oklch(0.68 0.15 28);
|
||||||
font-size: 18px;
|
font: var(--type-state-title);
|
||||||
letter-spacing: -0.035em;
|
letter-spacing: var(--type-state-title-tracking);
|
||||||
|
text-transform: var(--type-state-title-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-confirmation-dialog p {
|
.client-confirmation-dialog p {
|
||||||
max-width: 54ch;
|
max-width: 54ch;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 11px;
|
font: var(--type-body);
|
||||||
line-height: 1.7;
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-confirmation-actions {
|
.client-confirmation-actions {
|
||||||
@@ -608,8 +603,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-size: 10px;
|
font: var(--type-control);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-control-tracking);
|
||||||
|
text-transform: var(--type-control-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 200ms ease, filter 300ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: color 200ms ease, filter 300ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
@@ -656,14 +652,18 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
color: oklch(0.62 0.2 28);
|
color: oklch(0.62 0.2 28);
|
||||||
font: 600 11px/1.5 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-body);
|
||||||
|
letter-spacing: var(--type-body-tracking);
|
||||||
|
text-transform: var(--type-body-transform);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-inline-error small {
|
.client-inline-error small {
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,8 +673,9 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: color-mix(in oklch, var(--client-control) 72%, transparent);
|
background: color-mix(in oklch, var(--client-control) 72%, transparent);
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-size: 9px;
|
font: var(--type-label);
|
||||||
font-weight: 700;
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -709,8 +710,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font: 700 9px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: var(--type-label);
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: var(--type-label-tracking);
|
||||||
|
text-transform: var(--type-label-transform);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0.76;
|
opacity: 0.76;
|
||||||
transition: color 220ms ease, opacity 220ms ease, transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: color 220ms ease, opacity 220ms ease, transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
|||||||
+10
-16
@@ -24,7 +24,7 @@
|
|||||||
.client-power-section,
|
.client-power-section,
|
||||||
.client-gateway-traffic-chart,
|
.client-gateway-traffic-chart,
|
||||||
.client-gateway-traffic-freshness,
|
.client-gateway-traffic-freshness,
|
||||||
.client-subscription-drawer,
|
.client-drawer,
|
||||||
.client-form,
|
.client-form,
|
||||||
.client-server,
|
.client-server,
|
||||||
.client-server-auto,
|
.client-server-auto,
|
||||||
@@ -47,14 +47,13 @@
|
|||||||
.client-duration,
|
.client-duration,
|
||||||
.client-duration-seconds-value,
|
.client-duration-seconds-value,
|
||||||
.client-duration-second-digit,
|
.client-duration-second-digit,
|
||||||
.client-instructions,
|
|
||||||
.client-drawer-close,
|
.client-drawer-close,
|
||||||
.client-instructions-toggle,
|
.client-rail-action,
|
||||||
.client-instructions-toggle svg,
|
.client-rail-action svg,
|
||||||
.client-instructions-toggle circle,
|
.client-rail-action circle,
|
||||||
.client-instructions-toggle path,
|
.client-rail-action path,
|
||||||
.client-instructions-toggle rect,
|
.client-rail-action rect,
|
||||||
.client-instructions-toggle span,
|
.client-rail-action span,
|
||||||
.client-instruction-block,
|
.client-instruction-block,
|
||||||
.client-instruction-reveal,
|
.client-instruction-reveal,
|
||||||
.client-instruction-summary > i::before,
|
.client-instruction-summary > i::before,
|
||||||
@@ -114,11 +113,6 @@
|
|||||||
stroke-dashoffset: 0;
|
stroke-dashoffset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rules,
|
|
||||||
.client-local-rules-toggle,
|
|
||||||
.client-local-rules-toggle svg,
|
|
||||||
.client-local-rules-toggle circle,
|
|
||||||
.client-local-rules-toggle span,
|
|
||||||
.client-local-rule,
|
.client-local-rule,
|
||||||
.client-local-rule-enabled svg,
|
.client-local-rule-enabled svg,
|
||||||
.client-local-rule-enabled circle,
|
.client-local-rule-enabled circle,
|
||||||
@@ -128,9 +122,9 @@
|
|||||||
.client-rule-type-list,
|
.client-rule-type-list,
|
||||||
.client-rule-type-list button,
|
.client-rule-type-list button,
|
||||||
.client-local-rule input,
|
.client-local-rule input,
|
||||||
.client-local-rule-delete,
|
.client-row-delete,
|
||||||
.client-local-rule-add,
|
.client-row-add,
|
||||||
.client-local-rule-add-slot > span,
|
.client-row-add-slot > span,
|
||||||
.client-local-rules-save,
|
.client-local-rules-save,
|
||||||
.client-local-rules-runtime,
|
.client-local-rules-runtime,
|
||||||
.client-local-rules-actions button {
|
.client-local-rules-actions button {
|
||||||
|
|||||||
@@ -1,5 +1,77 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
|
--font-family-client: 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
||||||
|
--font-size-brand-tooltip: 0.25rem;
|
||||||
|
--font-size-brand-tooltip-strong: 0.2625rem;
|
||||||
|
--font-size-micro: 0.5rem;
|
||||||
|
--font-size-label: 0.5625rem;
|
||||||
|
--font-size-control: 0.625rem;
|
||||||
|
--font-size-body: 0.6875rem;
|
||||||
|
--font-size-data: 0.75rem;
|
||||||
|
--font-size-brand: 0.8125rem;
|
||||||
|
--font-size-item-title: 0.875rem;
|
||||||
|
--font-size-section-title: 1rem;
|
||||||
|
--font-size-icon-delete: 1.0625rem;
|
||||||
|
--font-size-state-title: 1.125rem;
|
||||||
|
--font-size-icon-chevron: 1.25rem;
|
||||||
|
--font-size-drawer-title: 1.375rem;
|
||||||
|
--font-size-icon-close: 1.5rem;
|
||||||
|
--font-weight-regular: 400;
|
||||||
|
--font-weight-body: 500;
|
||||||
|
--font-weight-strong: 600;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
--font-weight-brand: 800;
|
||||||
|
--line-height-solid: 1;
|
||||||
|
--line-height-micro: 1.45;
|
||||||
|
--line-height-label: 1.2;
|
||||||
|
--line-height-control: 1.2;
|
||||||
|
--line-height-body: 1.7;
|
||||||
|
--line-height-data: 1.2;
|
||||||
|
--line-height-item-title: 1.2;
|
||||||
|
--line-height-section-title: 1.25;
|
||||||
|
--line-height-state-title: 1.3;
|
||||||
|
--line-height-drawer-title: 1.15;
|
||||||
|
--line-height-tooltip: 1.35;
|
||||||
|
--tracking-tight: -0.04em;
|
||||||
|
--tracking-title: -0.03em;
|
||||||
|
--tracking-normal: 0;
|
||||||
|
--tracking-control: 0.03em;
|
||||||
|
--tracking-label: 0.08em;
|
||||||
|
--tracking-wide: 0.1em;
|
||||||
|
--text-transform-label: uppercase;
|
||||||
|
--text-transform-none: none;
|
||||||
|
--numeric-tabular: tabular-nums;
|
||||||
|
--type-micro: var(--font-weight-body) var(--font-size-micro)/var(--line-height-micro) var(--font-family-client);
|
||||||
|
--type-micro-tracking: var(--tracking-normal);
|
||||||
|
--type-micro-transform: var(--text-transform-none);
|
||||||
|
--type-label: var(--font-weight-bold) var(--font-size-label)/var(--line-height-label) var(--font-family-client);
|
||||||
|
--type-label-tracking: var(--tracking-label);
|
||||||
|
--type-label-transform: var(--text-transform-label);
|
||||||
|
--type-control: var(--font-weight-bold) var(--font-size-control)/var(--line-height-control) var(--font-family-client);
|
||||||
|
--type-control-tracking: var(--tracking-control);
|
||||||
|
--type-control-transform: var(--text-transform-none);
|
||||||
|
--type-tooltip: var(--font-weight-strong) var(--font-size-control)/var(--line-height-tooltip) var(--font-family-client);
|
||||||
|
--type-tooltip-tracking: var(--tracking-normal);
|
||||||
|
--type-tooltip-transform: var(--text-transform-none);
|
||||||
|
--type-body: var(--font-weight-body) var(--font-size-body)/var(--line-height-body) var(--font-family-client);
|
||||||
|
--type-body-tracking: var(--tracking-normal);
|
||||||
|
--type-body-transform: var(--text-transform-none);
|
||||||
|
--type-data: var(--font-weight-strong) var(--font-size-data)/var(--line-height-data) var(--font-family-client);
|
||||||
|
--type-data-tracking: var(--tracking-normal);
|
||||||
|
--type-data-transform: var(--text-transform-none);
|
||||||
|
--type-item-title: var(--font-weight-bold) var(--font-size-item-title)/var(--line-height-item-title) var(--font-family-client);
|
||||||
|
--type-item-title-tracking: var(--tracking-title);
|
||||||
|
--type-item-title-transform: var(--text-transform-none);
|
||||||
|
--type-section-title: var(--font-weight-bold) var(--font-size-section-title)/var(--line-height-section-title) var(--font-family-client);
|
||||||
|
--type-section-title-tracking: var(--tracking-title);
|
||||||
|
--type-section-title-transform: var(--text-transform-none);
|
||||||
|
--type-state-title: var(--font-weight-bold) var(--font-size-state-title)/var(--line-height-state-title) var(--font-family-client);
|
||||||
|
--type-state-title-tracking: var(--tracking-tight);
|
||||||
|
--type-state-title-transform: var(--text-transform-none);
|
||||||
|
--type-drawer-title: var(--font-weight-bold) var(--font-size-drawer-title)/var(--line-height-drawer-title) var(--font-family-client);
|
||||||
|
--type-drawer-title-tracking: var(--tracking-tight);
|
||||||
|
--type-drawer-title-transform: var(--text-transform-none);
|
||||||
|
--type-icon-close: var(--font-weight-regular) var(--font-size-icon-close)/var(--line-height-solid) var(--font-family-client);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app.client-app {
|
.app.client-app {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
interface CopyFeedback {
|
||||||
|
failed: boolean;
|
||||||
|
cycle?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CopyButtonProps {
|
||||||
|
label: string;
|
||||||
|
feedback?: CopyFeedback;
|
||||||
|
className?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
onClick: () => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CopyButton({
|
||||||
|
label,
|
||||||
|
feedback,
|
||||||
|
className = '',
|
||||||
|
ariaLabel,
|
||||||
|
onClick,
|
||||||
|
}: CopyButtonProps) {
|
||||||
|
return <button
|
||||||
|
className={`client-copy-button${className ? ` ${className}` : ''}${
|
||||||
|
feedback ? feedback.failed ? ' is-copy-error' : ' is-copied' : ''
|
||||||
|
}`}
|
||||||
|
type="button"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<span className="client-copy-label">{label}</span>
|
||||||
|
{feedback && <span className="client-copy-feedback" aria-hidden="true" key={feedback.cycle}>
|
||||||
|
{feedback.failed ? 'Ошибка' : 'Скопировано'}
|
||||||
|
</span>}
|
||||||
|
</button>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import type { ReactNode, RefObject } from 'react';
|
||||||
|
|
||||||
|
interface DrawerBaseProps {
|
||||||
|
panelRef: RefObject<HTMLElement | null>;
|
||||||
|
closeRef: RefObject<HTMLButtonElement | null>;
|
||||||
|
sheetRef?: RefObject<HTMLDivElement | null>;
|
||||||
|
id: string;
|
||||||
|
open: boolean;
|
||||||
|
className?: string;
|
||||||
|
sheetClassName?: string;
|
||||||
|
closeLabel: string;
|
||||||
|
onClose: () => unknown;
|
||||||
|
leading?: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DrawerProps = DrawerBaseProps & (
|
||||||
|
| { labelledBy: string; label?: never }
|
||||||
|
| { label: string; labelledBy?: never }
|
||||||
|
);
|
||||||
|
|
||||||
|
export function Drawer({
|
||||||
|
panelRef,
|
||||||
|
closeRef,
|
||||||
|
sheetRef,
|
||||||
|
id,
|
||||||
|
open,
|
||||||
|
className = '',
|
||||||
|
sheetClassName = '',
|
||||||
|
labelledBy,
|
||||||
|
label,
|
||||||
|
closeLabel,
|
||||||
|
onClose,
|
||||||
|
leading,
|
||||||
|
children,
|
||||||
|
}: DrawerProps) {
|
||||||
|
return <aside
|
||||||
|
ref={panelRef}
|
||||||
|
id={id}
|
||||||
|
className={`client-drawer${className ? ` ${className}` : ''}${open ? ' is-open' : ''}`}
|
||||||
|
aria-labelledby={labelledBy}
|
||||||
|
aria-label={label}
|
||||||
|
aria-hidden={!open}
|
||||||
|
inert={!open ? true : undefined}
|
||||||
|
>
|
||||||
|
<div ref={sheetRef} className={`client-drawer-sheet${sheetClassName ? ` ${sheetClassName}` : ''}`}>
|
||||||
|
{leading}
|
||||||
|
<button
|
||||||
|
ref={closeRef}
|
||||||
|
className="client-drawer-close"
|
||||||
|
type="button"
|
||||||
|
aria-label={closeLabel}
|
||||||
|
onClick={onClose}
|
||||||
|
>×</button>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</aside>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { ReactNode, RefObject } from 'react';
|
||||||
|
|
||||||
|
interface RailActionProps {
|
||||||
|
buttonRef: RefObject<HTMLButtonElement | null>;
|
||||||
|
className?: string;
|
||||||
|
open: boolean;
|
||||||
|
controls: string;
|
||||||
|
ariaLabel: string;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onClick: () => unknown;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RailAction({
|
||||||
|
buttonRef,
|
||||||
|
className = '',
|
||||||
|
open,
|
||||||
|
controls,
|
||||||
|
ariaLabel,
|
||||||
|
label,
|
||||||
|
disabled = false,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
}: RailActionProps) {
|
||||||
|
return <button
|
||||||
|
ref={buttonRef}
|
||||||
|
className={`client-rail-action${className ? ` ${className}` : ''}${open ? ' is-open' : ''}`}
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls={controls}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<span>{label}</span>
|
||||||
|
</button>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
children: ReactNode;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tooltip({ children, id }: TooltipProps) {
|
||||||
|
return <span className="client-tooltip" id={id} role="tooltip">{children}</span>;
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ test('shared clock, copy feedback and live announcement stay single-owned by the
|
|||||||
assert.match(page, /onCopyProxy=\{copyProxy\}/);
|
assert.match(page, /onCopyProxy=\{copyProxy\}/);
|
||||||
assert.match(page, /copyTimersRef = useRef<Partial<Record<CopyKind[\s\S]*copyTimersRef\.current\[kind\]/);
|
assert.match(page, /copyTimersRef = useRef<Partial<Record<CopyKind[\s\S]*copyTimersRef\.current\[kind\]/);
|
||||||
assert.match(page, /copyAttemptsRef = useRef<Partial<Record<CopyKind[\s\S]*copyAttemptsRef\.current\[kind\] = attempt[\s\S]*await copyText\(value\)[\s\S]*copyAttemptsRef\.current\[kind\] !== attempt[\s\S]*const pendingTimer = copyTimersRef\.current\[kind\][\s\S]*setCopyFeedback/);
|
assert.match(page, /copyAttemptsRef = useRef<Partial<Record<CopyKind[\s\S]*copyAttemptsRef\.current\[kind\] = attempt[\s\S]*await copyText\(value\)[\s\S]*copyAttemptsRef\.current\[kind\] !== attempt[\s\S]*const pendingTimer = copyTimersRef\.current\[kind\][\s\S]*setCopyFeedback/);
|
||||||
assert.match(panel, /const feedback = copyFeedback\?\.\[kind\][\s\S]*key=\{feedback\.cycle\}/);
|
assert.match(panel, /const feedback = copyFeedback\?\.\[kind\][\s\S]*<CopyButton[\s\S]*feedback=\{feedback\}/);
|
||||||
assert.match(panel, /localProxyUrls\(proxyPort, gatewayAddress\)/);
|
assert.match(panel, /localProxyUrls\(proxyPort, gatewayAddress\)/);
|
||||||
assert.match(panel, /onClick=\{\(\) => onCopyProxy\(kind\)\}/);
|
assert.match(panel, /onClick=\{\(\) => onCopyProxy\(kind\)\}/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(panel, /className=\{`client-device-policy is-\$\{displayPolicy\}/);
|
assert.match(panel, /className=\{`client-device-policy is-\$\{displayPolicy\}/);
|
||||||
assert.match(panel, /displayPolicy === 'direct' \? <svg[\s\S]*M4 12h15M14 7l5 5-5 5[\s\S]*M12 3 19 6v5/);
|
assert.match(panel, /displayPolicy === 'direct' \? <svg[\s\S]*M4 12h15M14 7l5 5-5 5[\s\S]*M12 3 19 6v5/);
|
||||||
assert.match(panel, /Полностью обходит sing-box/);
|
assert.match(panel, /Полностью обходит sing-box/);
|
||||||
assert.match(panel, /client-drawer client-instructions client-devices/);
|
assert.match(panel, /<Drawer[\s\S]*className="client-instructions client-devices"/);
|
||||||
assert.doesNotMatch(panel, /точная MAC|частная MAC|<dt>Источник<\/dt>/);
|
assert.doesNotMatch(panel, /точная MAC|частная MAC|<dt>Источник<\/dt>/);
|
||||||
assert.match(panel, /aria-pressed=\{device\.pinned\}/);
|
assert.match(panel, /aria-pressed=\{device\.pinned\}/);
|
||||||
assert.match(panel, /const \[pinCollapses, setPinCollapses\][\s\S]*animationDone[\s\S]*result: saved \? 'saved' : 'failed'/);
|
assert.match(panel, /const \[pinCollapses, setPinCollapses\][\s\S]*animationDone[\s\S]*result: saved \? 'saved' : 'failed'/);
|
||||||
@@ -131,7 +131,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(styles, /\.client-device\.is-deprioritized \.client-device-main \{[\s\S]*grid-column: 1 \/ 4/);
|
assert.match(styles, /\.client-device\.is-deprioritized \.client-device-main \{[\s\S]*grid-column: 1 \/ 4/);
|
||||||
assert.match(styles, /\.client-device-deprioritize-wrap \{[\s\S]*grid-column: 4;[\s\S]*grid-row: 2/);
|
assert.match(styles, /\.client-device-deprioritize-wrap \{[\s\S]*grid-column: 4;[\s\S]*grid-row: 2/);
|
||||||
assert.match(styles, /\.client-device\.is-deprioritized \.client-device-deprioritize-wrap \{[\s\S]*grid-row: 1/);
|
assert.match(styles, /\.client-device\.is-deprioritized \.client-device-deprioritize-wrap \{[\s\S]*grid-row: 1/);
|
||||||
assert.match(styles, /\.client-device-group-heading \{[\s\S]*text-transform: uppercase/);
|
assert.match(styles, /\.client-device-group-heading \{[\s\S]*text-transform: var\(--type-label-transform\)/);
|
||||||
assert.match(styles, /\.client-device\.is-pinned \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-expand 520ms/);
|
assert.match(styles, /\.client-device\.is-pinned \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-expand 520ms/);
|
||||||
assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-collapse 520ms/);
|
assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-collapse 520ms/);
|
||||||
assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-axis \{[\s\S]*top: 0;[\s\S]*height: 58px/);
|
assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-axis \{[\s\S]*top: 0;[\s\S]*height: 58px/);
|
||||||
@@ -156,13 +156,13 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(styles, /@keyframes client-device-traffic-detail-in[\s\S]*opacity: 0[\s\S]*opacity: 1/);
|
assert.match(styles, /@keyframes client-device-traffic-detail-in[\s\S]*opacity: 0[\s\S]*opacity: 1/);
|
||||||
assert.match(styles, /@keyframes client-device-traffic-detail-out[\s\S]*opacity: 1[\s\S]*opacity: 0/);
|
assert.match(styles, /@keyframes client-device-traffic-detail-out[\s\S]*opacity: 1[\s\S]*opacity: 0/);
|
||||||
assert.match(styles, /\.client-device-policy \{[\s\S]*width: 34px;[\s\S]*border-radius: 50%/);
|
assert.match(styles, /\.client-device-policy \{[\s\S]*width: 34px;[\s\S]*border-radius: 50%/);
|
||||||
assert.match(styles, /\.client-device-alias-trigger \{[\s\S]*font: 700 14px\/1\.2/);
|
assert.match(styles, /\.client-device-alias-trigger \{[\s\S]*font: var\(--type-item-title\)/);
|
||||||
assert.match(styles, /\.client-device-alias-input \{[\s\S]*width: var\(--alias-width\)[\s\S]*caret-color: var\(--client-accent\)[\s\S]*client-device-alias-edit-in 360ms/);
|
assert.match(styles, /\.client-device-alias-input \{[\s\S]*width: var\(--alias-width\)[\s\S]*caret-color: var\(--client-accent\)[\s\S]*client-device-alias-edit-in 360ms/);
|
||||||
assert.match(styles, /@keyframes client-device-alias-edit-in[\s\S]*color: var\(--client-accent\)[\s\S]*filter: blur\(2px\)/);
|
assert.match(styles, /@keyframes client-device-alias-edit-in[\s\S]*color: var\(--client-accent\)[\s\S]*filter: blur\(2px\)/);
|
||||||
assert.doesNotMatch(styles, /\.client-device-alias \{/);
|
assert.doesNotMatch(styles, /\.client-device-alias \{/);
|
||||||
assert.match(styles, /\.client-device-ip \{[\s\S]*font: 600 12px\/1\.2/);
|
assert.match(styles, /\.client-device-ip \{[\s\S]*font: var\(--type-data\)/);
|
||||||
assert.match(styles, /\.client-device-last-seen \{[\s\S]*position: absolute;[\s\S]*top: 0;[\s\S]*left: 0;[\s\S]*font-size: 8\.5px/);
|
assert.match(styles, /\.client-device-last-seen \{[\s\S]*position: absolute;[\s\S]*top: 0;[\s\S]*left: 0;[\s\S]*font: var\(--type-micro\)/);
|
||||||
assert.match(styles, /\.client-device-traffic strong \{[\s\S]*font-size: 10px/);
|
assert.match(styles, /\.client-device-traffic strong \{[\s\S]*font: var\(--type-control\)/);
|
||||||
assert.match(styles, /\.client-device-name-separator \{[\s\S]*color: var\(--client-muted\)/);
|
assert.match(styles, /\.client-device-name-separator \{[\s\S]*color: var\(--client-muted\)/);
|
||||||
assert.match(styles, /\.client-device-ip\.is-copied \{[\s\S]*client-device-ip-copy 800ms/);
|
assert.match(styles, /\.client-device-ip\.is-copied \{[\s\S]*client-device-ip-copy 800ms/);
|
||||||
assert.match(styles, /\.client-device-main \{[^}]*height: 34px[\s\S]*align-items: flex-end[\s\S]*padding: 11px 0 0/);
|
assert.match(styles, /\.client-device-main \{[^}]*height: 34px[\s\S]*align-items: flex-end[\s\S]*padding: 11px 0 0/);
|
||||||
@@ -214,7 +214,7 @@ test('Gateway Home reuses the canonical device snapshot for applied route and gl
|
|||||||
assert.match(feature, /feature\.status === 'error' \? feature\.error : null/);
|
assert.match(feature, /feature\.status === 'error' \? feature\.error : null/);
|
||||||
assert.match(overview, /if \(!isGateway && \(!connected \|\| !state\?\.connection\?\.startedAt\)\) return undefined/);
|
assert.match(overview, /if \(!isGateway && \(!connected \|\| !state\?\.connection\?\.startedAt\)\) return undefined/);
|
||||||
assert.match(feature, /trafficSourceError[\s\S]*Трафик не обновляется · последние данные/);
|
assert.match(feature, /trafficSourceError[\s\S]*Трафик не обновляется · последние данные/);
|
||||||
assert.match(subscription, /client-drawer client-subscription-drawer\$\{drawerOpen \? ' is-open' : ''\}/);
|
assert.match(subscription, /<Drawer[\s\S]*className="client-subscription-drawer"[\s\S]*open=\{drawerOpen\}/);
|
||||||
assert.match(subscription, /const deleteIdRef = useRef\(deleteId\)[\s\S]*if \(deleteIdRef\.current\) return/);
|
assert.match(subscription, /const deleteIdRef = useRef\(deleteId\)[\s\S]*if \(deleteIdRef\.current\) return/);
|
||||||
assert.match(subscription, /requestDelete: \(profileId: string\) => setDeleteId\(profileId\)[\s\S]*open=\{Boolean\(feature\.deleteProfile\)\}[\s\S]*onCancel=\{feature\.cancelDelete\}[\s\S]*onConfirm=\{feature\.confirmDelete\}/);
|
assert.match(subscription, /requestDelete: \(profileId: string\) => setDeleteId\(profileId\)[\s\S]*open=\{Boolean\(feature\.deleteProfile\)\}[\s\S]*onCancel=\{feature\.cancelDelete\}[\s\S]*onConfirm=\{feature\.confirmDelete\}/);
|
||||||
assert.match(connection, /aria-label=\{remoteOwned[\s\S]*isGateway[\s\S]*Остановить Harbor Connect[\s\S]*Запустить Harbor Connect/);
|
assert.match(connection, /aria-label=\{remoteOwned[\s\S]*isGateway[\s\S]*Остановить Harbor Connect[\s\S]*Запустить Harbor Connect/);
|
||||||
|
|||||||
@@ -48,5 +48,5 @@ test('private content helpers preserve guide, copy and monitoring contracts', ()
|
|||||||
assert.match(prometheus, /\.\.\/\.\.\/\.\.\/\.\.\/monitoring\/grafana\/harbor-gateway\.json/);
|
assert.match(prometheus, /\.\.\/\.\.\/\.\.\/\.\.\/monitoring\/grafana\/harbor-gateway\.json/);
|
||||||
assert.match(prometheus, /scrape_interval: 30s[\s\S]*scrape_timeout: 3s[\s\S]*metrics_path: \/metrics/);
|
assert.match(prometheus, /scrape_interval: 30s[\s\S]*scrape_timeout: 3s[\s\S]*metrics_path: \/metrics/);
|
||||||
assert.match(feature, /target="_blank" rel="noreferrer"/);
|
assert.match(feature, /target="_blank" rel="noreferrer"/);
|
||||||
assert.match(feature, /client-copy-label">Скопировать/);
|
assert.match(feature, /<CopyButton[\s\S]*label="Скопировать"/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ test('Gateway info drawer contains copyable Prometheus and Grafana instructions'
|
|||||||
assert.match(prometheus, /monitoring\/grafana\/harbor-gateway\.json/);
|
assert.match(prometheus, /monitoring\/grafana\/harbor-gateway\.json/);
|
||||||
assert.match(prometheus, /scrape_interval: 30s[\s\S]*metrics_path: \/metrics/);
|
assert.match(prometheus, /scrape_interval: 30s[\s\S]*metrics_path: \/metrics/);
|
||||||
assert.match(overview, /const controlHost = window\.location\.host/);
|
assert.match(overview, /const controlHost = window\.location\.host/);
|
||||||
assert.match(feature, /client-copy-label">Скопировать/);
|
assert.match(feature, /<CopyButton[\s\S]*label="Скопировать"/);
|
||||||
assert.doesNotMatch(`${overview}\n${feature}`, /prometheus-toggle|Prometheus<\/span><\/button>/);
|
assert.doesNotMatch(`${overview}\n${feature}`, /prometheus-toggle|Prometheus<\/span><\/button>/);
|
||||||
assert.match(styles, /\.client-instruction-copy-button \{[\s\S]*width: 104px;[\s\S]*min-width: 104px/);
|
assert.match(styles, /\.client-instruction-copy-button \{[\s\S]*width: 104px;[\s\S]*min-width: 104px/);
|
||||||
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-copy-feedback/);
|
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-copy-feedback/);
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ test('secondary menus share one right rail and both drawers open from the right'
|
|||||||
|
|
||||||
assert.match(component, /<nav className="client-secondary-menu" aria-label="Дополнительные меню">/);
|
assert.match(component, /<nav className="client-secondary-menu" aria-label="Дополнительные меню">/);
|
||||||
assert.match(component, /<SubscriptionToggle[\s\S]*<InstructionsToggle[\s\S]*<DevicesToggle/);
|
assert.match(component, /<SubscriptionToggle[\s\S]*<InstructionsToggle[\s\S]*<DevicesToggle/);
|
||||||
assert.match(subscription, /aria-controls="client-subscription-drawer"/);
|
assert.match(subscription, /controls="client-subscription-drawer"/);
|
||||||
assert.match(component, /<InstructionsToggle[\s\S]*<RoutingToggle/);
|
assert.match(component, /<InstructionsToggle[\s\S]*<RoutingToggle/);
|
||||||
assert.match(component, /<InstructionsToggle[\s\S]*<DevicesToggle[\s\S]*<DiagnosticsToggle[\s\S]*<RoutingToggle/);
|
assert.match(component, /<InstructionsToggle[\s\S]*<DevicesToggle[\s\S]*<DiagnosticsToggle[\s\S]*<RoutingToggle/);
|
||||||
assert.match(diagnosticsFeature, /client-diagnostics-toggle/);
|
assert.match(diagnosticsFeature, /client-diagnostics-toggle/);
|
||||||
@@ -166,9 +166,9 @@ test('secondary menus share one right rail and both drawers open from the right'
|
|||||||
);
|
);
|
||||||
assert.match(rule('.client-instructions'), /width:\s*min\(470px, 100vw\)/);
|
assert.match(rule('.client-instructions'), /width:\s*min\(470px, 100vw\)/);
|
||||||
assert.match(rule('.client-local-rules'), /width:\s*min\(480px, 100vw\)/);
|
assert.match(rule('.client-local-rules'), /width:\s*min\(480px, 100vw\)/);
|
||||||
assert.match(instructions, /className={`client-drawer client-instructions/);
|
assert.match(instructions, /<Drawer[\s\S]*className="client-instructions"/);
|
||||||
assert.match(routing, /className={`client-drawer client-local-rules/);
|
assert.match(routing, /<Drawer[\s\S]*className="client-local-rules"/);
|
||||||
assert.match(subscription, /client-drawer client-subscription-drawer/);
|
assert.match(subscription, /<Drawer[\s\S]*className="client-subscription-drawer"/);
|
||||||
assert.match(component, /subscriptionFeature\.close\(\)[\s\S]*devicesFeature\.close\(\)[\s\S]*diagnosticsFeature\.close\(\)[\s\S]*instructionsFeature\.toggle\(\)/);
|
assert.match(component, /subscriptionFeature\.close\(\)[\s\S]*devicesFeature\.close\(\)[\s\S]*diagnosticsFeature\.close\(\)[\s\S]*instructionsFeature\.toggle\(\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ test('connectivity diagnostics render stable compact tables before the first run
|
|||||||
assert.match(diagnostics, /className="client-diagnostics-service-table" role="table"/);
|
assert.match(diagnostics, /className="client-diagnostics-service-table" role="table"/);
|
||||||
assert.match(diagnostics, /\+ Добавить сервис/);
|
assert.match(diagnostics, /\+ Добавить сервис/);
|
||||||
assert.match(diagnostics, /HIDDEN_SERVICES_KEY/);
|
assert.match(diagnostics, /HIDDEN_SERVICES_KEY/);
|
||||||
assert.match(diagnostics, /className="client-local-rule-delete"[\s\S]*Удалить сервис \$\{site\.label\}/);
|
assert.match(diagnostics, /className="client-row-delete"[\s\S]*Удалить сервис \$\{site\.label\}/);
|
||||||
assert.match(diagnostics, /client-deletable-row[\s\S]*className="client-delete-strike"[\s\S]*onAnimationEnd/);
|
assert.match(diagnostics, /client-deletable-row[\s\S]*className="client-delete-strike"[\s\S]*onAnimationEnd/);
|
||||||
assert.match(diagnostics, /document\.startViewTransition\(update\)/);
|
assert.match(diagnostics, /document\.startViewTransition\(update\)/);
|
||||||
assert.match(diagnostics, /client-diagnostics-refresh/);
|
assert.match(diagnostics, /client-diagnostics-refresh/);
|
||||||
@@ -221,9 +221,9 @@ test('responsive motion has a reduced-motion fallback', () => {
|
|||||||
assert.match(reducedMotion, /\.client-power-section/);
|
assert.match(reducedMotion, /\.client-power-section/);
|
||||||
assert.match(reducedMotion, /\.client-form/);
|
assert.match(reducedMotion, /\.client-form/);
|
||||||
assert.match(reducedMotion, /\.harbor-brand/);
|
assert.match(reducedMotion, /\.harbor-brand/);
|
||||||
assert.match(reducedMotion, /\.client-instructions-toggle circle/);
|
assert.match(reducedMotion, /\.client-rail-action circle/);
|
||||||
assert.match(reducedMotion, /\.client-instructions-toggle rect/);
|
assert.match(reducedMotion, /\.client-rail-action rect/);
|
||||||
assert.match(reducedMotion, /\.client-local-rules-toggle circle/);
|
assert.match(reducedMotion, /\.client-drawer/);
|
||||||
assert.match(reducedMotion, /transition:\s*none/);
|
assert.match(reducedMotion, /transition:\s*none/);
|
||||||
assert.match(reducedMotion, /animation:\s*none/);
|
assert.match(reducedMotion, /animation:\s*none/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const subscription = fs.readFileSync(path.join(root, 'src/web/features/subscript
|
|||||||
const routing = fs.readFileSync(path.join(root, 'src/web/features/routing/RoutingFeature.tsx'), 'utf8');
|
const routing = fs.readFileSync(path.join(root, 'src/web/features/routing/RoutingFeature.tsx'), 'utf8');
|
||||||
const instructions = fs.readFileSync(path.join(root, 'src/web/features/instructions/InstructionsFeature.tsx'), 'utf8');
|
const instructions = fs.readFileSync(path.join(root, 'src/web/features/instructions/InstructionsFeature.tsx'), 'utf8');
|
||||||
const dialog = fs.readFileSync(path.join(root, 'src/web/ui/ConfirmationDialog.tsx'), 'utf8');
|
const dialog = fs.readFileSync(path.join(root, 'src/web/ui/ConfirmationDialog.tsx'), 'utf8');
|
||||||
|
const copyButton = fs.readFileSync(path.join(root, 'src/web/ui/CopyButton.tsx'), 'utf8');
|
||||||
const styles = readStyleSource(root);
|
const styles = readStyleSource(root);
|
||||||
|
|
||||||
test('rule editor add latency stays constant and dirty exits are guarded', () => {
|
test('rule editor add latency stays constant and dirty exits are guarded', () => {
|
||||||
@@ -31,7 +32,7 @@ test('rule editor add latency stays constant and dirty exits are guarded', () =>
|
|||||||
assert.match(routing, /client-deletable-row/);
|
assert.match(routing, /client-deletable-row/);
|
||||||
assert.match(routing, /className="client-delete-strike"[\s\S]*onAnimationEnd/);
|
assert.match(routing, /className="client-delete-strike"[\s\S]*onAnimationEnd/);
|
||||||
assert.doesNotMatch(routing, /client-rule-delete-cross/);
|
assert.doesNotMatch(routing, /client-rule-delete-cross/);
|
||||||
assert.match(routing, /className="client-local-rule-delete"/);
|
assert.match(routing, /className="client-row-delete"/);
|
||||||
assert.match(styles, /\.client-deletable-row\.is-removing > \.client-delete-strike[\s\S]*client-delete-strike/);
|
assert.match(styles, /\.client-deletable-row\.is-removing > \.client-delete-strike[\s\S]*client-delete-strike/);
|
||||||
assert.match(styles, /\.client-delete-strike \{[\s\S]*z-index: 100[\s\S]*background: transparent/);
|
assert.match(styles, /\.client-delete-strike \{[\s\S]*z-index: 100[\s\S]*background: transparent/);
|
||||||
assert.match(styles, /\.client-deletable-row\.is-removing > :not\(\.client-delete-strike\)[\s\S]*z-index: 0/);
|
assert.match(styles, /\.client-deletable-row\.is-removing > :not\(\.client-delete-strike\)[\s\S]*z-index: 0/);
|
||||||
@@ -40,7 +41,7 @@ test('rule editor add latency stays constant and dirty exits are guarded', () =>
|
|||||||
assert.doesNotMatch(styles, /client-local-rule > button:last-child/);
|
assert.doesNotMatch(styles, /client-local-rule > button:last-child/);
|
||||||
assert.match(styles, /@keyframes client-delete-strike[\s\S]*scaleX\(0\)[\s\S]*scaleX\(1\)/);
|
assert.match(styles, /@keyframes client-delete-strike[\s\S]*scaleX\(0\)[\s\S]*scaleX\(1\)/);
|
||||||
assert.match(styles, /53%[\s\S]*transform-origin: right center[\s\S]*100%[\s\S]*scaleX\(0\)/);
|
assert.match(styles, /53%[\s\S]*transform-origin: right center[\s\S]*100%[\s\S]*scaleX\(0\)/);
|
||||||
assert.match(styles, /@keyframes client-local-rule-leave[\s\S]*translateX\(28px\)/);
|
assert.match(styles, /@keyframes client-row-leave[\s\S]*translateX\(28px\)/);
|
||||||
assert.match(styles, /@keyframes client-delete-content-dim[\s\S]*opacity: 0\.58[\s\S]*blur\(1\.4px\)[\s\S]*opacity: 0/);
|
assert.match(styles, /@keyframes client-delete-content-dim[\s\S]*opacity: 0\.58[\s\S]*blur\(1\.4px\)[\s\S]*opacity: 0/);
|
||||||
assert.match(styles, /\.client-local-rule:has\(\.client-rule-type\.is-open\)[\s\S]*z-index: 5/);
|
assert.match(styles, /\.client-local-rule:has\(\.client-rule-type\.is-open\)[\s\S]*z-index: 5/);
|
||||||
});
|
});
|
||||||
@@ -84,16 +85,16 @@ test('copy feedback, drawers and Gateway access actions expose complete semantic
|
|||||||
assert.match(component, /className="client-live-region" role="status" aria-live="polite" aria-atomic="true"/);
|
assert.match(component, /className="client-live-region" role="status" aria-live="polite" aria-atomic="true"/);
|
||||||
assert.match(component, /Не удалось скопировать/);
|
assert.match(component, /Не удалось скопировать/);
|
||||||
assert.match(component, /Скопировано/);
|
assert.match(component, /Скопировано/);
|
||||||
assert.match(connection, /client-copy-feedback[^\n]*\{feedback\.failed \? 'Ошибка' : 'Скопировано'\}/);
|
assert.match(copyButton, /client-copy-feedback[\s\S]*feedback\.failed \? 'Ошибка' : 'Скопировано'/);
|
||||||
assert.doesNotMatch(component, /ГОТОВО/);
|
assert.doesNotMatch(component, /ГОТОВО/);
|
||||||
assert.doesNotMatch(component, />Error<|>Copied</);
|
assert.doesNotMatch(component, />Error<|>Copied</);
|
||||||
assert.match(instructions, /aria-label="Закрыть инструкции"/);
|
assert.match(instructions, /closeLabel="Закрыть инструкции"/);
|
||||||
assert.match(routing, /aria-label="Закрыть локальные правила"/);
|
assert.match(routing, /closeLabel="Закрыть локальные правила"/);
|
||||||
assert.match(instructions, /closeRef\.current\?\.focus\(\)/);
|
assert.match(instructions, /closeRef\.current\?\.focus\(\)/);
|
||||||
assert.match(routing, /closeRef\.current\?\.focus\(\)/);
|
assert.match(routing, /closeRef\.current\?\.focus\(\)/);
|
||||||
assert.match(connection, /aria-label={`Скопировать \$\{label\}: \$\{kind === 'gateway' \? gatewayAddress : proxyUrls\[kind\]\}`}/);
|
assert.match(connection, /ariaLabel={`Скопировать \$\{label\}: \$\{kind === 'gateway' \? gatewayAddress : proxyUrls\[kind\]\}`}/);
|
||||||
assert.doesNotMatch(component, /client-access-tabs|role="tab"|role="tabpanel"/);
|
assert.doesNotMatch(component, /client-access-tabs|role="tab"|role="tabpanel"/);
|
||||||
assert.match(styles, /\.client-drawer-close \{[\s\S]*width: 44px;[\s\S]*height: 44px/);
|
assert.match(styles, /\.client-drawer-close \{[\s\S]*width: 44px;[\s\S]*height: 44px/);
|
||||||
assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-copy-button \{[\s\S]*min-height: 44px/);
|
assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-copy-button \{[\s\S]*min-height: 44px/);
|
||||||
assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-local-rule-enabled,[\s\S]*\.client-local-rule-delete \{[\s\S]*width: 44px;[\s\S]*height: 44px/);
|
assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-local-rule-enabled,[\s\S]*\.client-row-delete \{[\s\S]*width: 44px;[\s\S]*height: 44px/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ test('manual ping uses plain language and keeps results beside server names', ()
|
|||||||
assert.match(picker, /is-scalable\$\{simpleServers\.length <= INLINE_SERVER_ROWS \? ' is-short' : ''\}/);
|
assert.match(picker, /is-scalable\$\{simpleServers\.length <= INLINE_SERVER_ROWS \? ' is-short' : ''\}/);
|
||||||
assert.match(styles, /\.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 120px minmax\(0, 1fr\);/);
|
assert.match(styles, /\.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 120px minmax\(0, 1fr\);/);
|
||||||
assert.match(styles, /\.client-server-row \.client-server \{[\s\S]*?width: 120px;/);
|
assert.match(styles, /\.client-server-row \.client-server \{[\s\S]*?width: 120px;/);
|
||||||
assert.match(styles, /\.client-server-health \{[\s\S]*?font-size: 9px;[\s\S]*?font-variant-numeric: tabular-nums;/);
|
assert.match(styles, /\.client-server-health \{[\s\S]*?font: var\(--type-label\);[\s\S]*?font-variant-numeric: var\(--numeric-tabular\);/);
|
||||||
assert.match(styles, /\.client-server-meta \.client-server-health \{[\s\S]*?place-items: end start;[\s\S]*?padding-bottom: 7px;/);
|
assert.match(styles, /\.client-server-meta \.client-server-health \{[\s\S]*?place-items: end start;[\s\S]*?padding-bottom: 7px;/);
|
||||||
assert.match(styles, /\.client-server-health\.is-checking \.client-server-health-checking \{[\s\S]*?animation: client-spin 900ms linear infinite;/);
|
assert.match(styles, /\.client-server-health\.is-checking \.client-server-health-checking \{[\s\S]*?animation: client-spin 900ms linear infinite;/);
|
||||||
assert.match(styles, /\.client-server-check\.is-checking svg \{[\s\S]*?animation: client-spin 900ms linear infinite;/);
|
assert.match(styles, /\.client-server-check\.is-checking svg \{[\s\S]*?animation: client-spin 900ms linear infinite;/);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import crypto from 'node:crypto';
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
import postcss from 'postcss';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
analyzeSelectorList,
|
analyzeSelectorList,
|
||||||
@@ -36,26 +37,26 @@ const expectedImports = [
|
|||||||
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||||
const acceptedLedger = {
|
const acceptedLedger = {
|
||||||
counts: {
|
counts: {
|
||||||
cascadeEdges: 819,
|
cascadeEdges: 749,
|
||||||
customProperties: 31,
|
customProperties: 103,
|
||||||
declarations: 2975,
|
declarations: 3193,
|
||||||
important: 0,
|
important: 0,
|
||||||
keyframes: 56,
|
keyframes: 56,
|
||||||
media: 13,
|
media: 13,
|
||||||
rules: 919,
|
rules: 916,
|
||||||
variableReferences: 328,
|
variableReferences: 788,
|
||||||
},
|
},
|
||||||
hashes: {
|
hashes: {
|
||||||
cascadeEdges: '1a3882545e488b80ac5454cc8fa236fd242d15eb8a27ac3371876af866b6c6e8',
|
cascadeEdges: '1f48ed1c4272092b7698d94ea8d0fc1ebfb66454a6a715c9b684515bbff74fb8',
|
||||||
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
customProperties: 'fc8401b40b8d2cc8a1fc1716360ba8e5baccb694cdabde68427d3c92c04a84d4',
|
||||||
declarations: '3972e7fd7da24d872a16cfa9dad9e9355861f489708fd9643a8610422e712825',
|
declarations: '8507f8575ae0bd512449c411f0a553f6478ab18fdf217534a32617a8abb9aeb2',
|
||||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||||
duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d',
|
duplicateSelectors: '419962e669cbfdf5c8186318cb26688477fc0199cae98df8e1cd9bf5cddd76c2',
|
||||||
keyframes: '39b6fc9c9ab451d63bb77afd90caf63d2d74a30cb351a8f86503de380d76a936',
|
keyframes: 'fb859c4d0d1bfd2f5a18904e30a5f79c6ce6931d74fe88cbd506fb28c334b7ac',
|
||||||
ruleDeclarationSequences: '99469fb4eeb3af72a12cc4365f135e988ab95ebcf194572779d62e5185778d20',
|
ruleDeclarationSequences: 'a1d47956771b1503fad124768e3157fe924c6c76914d219885e2cc2dab4413bb',
|
||||||
selectors: 'b2564f9e6c078233b388e84680181cbd5f21d55e963907990b237b14c5bc250f',
|
selectors: '6dd352f47606c21b5c07817e185753baa8b3168ab92d3cfef4b28f772b989cc7',
|
||||||
variableReferences: 'c8348faf26b2174f02b9760aaa5812e22e02a72da7902fb5897f0c23e7f4e939',
|
variableReferences: '700137c05be5f12722a7ebec90d06be70dde203a754a9312d45c03ff05ff08ec',
|
||||||
witnesses: '3c44c91b2cc2a03049b328b0f7a974b1c189c66d2a778dbb386601eed82994e8',
|
witnesses: 'f51f070cc2c7dba0f8d08518cdeee49eaf176aac82e3571cdd8498315ba8db83',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ test('tokens, shared primitives, and feature styles have one explicit owner', ()
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const keyframe of [
|
for (const keyframe of [
|
||||||
'client-local-rule-enter', 'client-local-rule-leave', 'client-delete-strike',
|
'client-row-enter', 'client-row-leave', 'client-delete-strike',
|
||||||
'client-delete-content-dim', 'client-spin', 'client-copy-fade',
|
'client-delete-content-dim', 'client-spin', 'client-copy-fade',
|
||||||
]) {
|
]) {
|
||||||
const owners = Object.entries(sources)
|
const owners = Object.entries(sources)
|
||||||
@@ -108,6 +109,104 @@ test('tokens, shared primitives, and feature styles have one explicit owner', ()
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('client typography uses the shared semantic scale outside the token owner', () => {
|
||||||
|
const tokenRoot = postcss.parse(fs.readFileSync(path.join(stylesRoot, 'tokens.css'), 'utf8'));
|
||||||
|
const tokens = new Map();
|
||||||
|
tokenRoot.walkDecls((declaration) => tokens.set(declaration.prop, declaration.value));
|
||||||
|
const roleValues = {
|
||||||
|
micro: ['0.5rem', 'var(--font-weight-body) var(--font-size-micro)/var(--line-height-micro) var(--font-family-client)', '1.45', 'var(--tracking-normal)', 'var(--text-transform-none)'],
|
||||||
|
label: ['0.5625rem', 'var(--font-weight-bold) var(--font-size-label)/var(--line-height-label) var(--font-family-client)', '1.2', 'var(--tracking-label)', 'var(--text-transform-label)'],
|
||||||
|
control: ['0.625rem', 'var(--font-weight-bold) var(--font-size-control)/var(--line-height-control) var(--font-family-client)', '1.2', 'var(--tracking-control)', 'var(--text-transform-none)'],
|
||||||
|
body: ['0.6875rem', 'var(--font-weight-body) var(--font-size-body)/var(--line-height-body) var(--font-family-client)', '1.7', 'var(--tracking-normal)', 'var(--text-transform-none)'],
|
||||||
|
data: ['0.75rem', 'var(--font-weight-strong) var(--font-size-data)/var(--line-height-data) var(--font-family-client)', '1.2', 'var(--tracking-normal)', 'var(--text-transform-none)'],
|
||||||
|
'item-title': ['0.875rem', 'var(--font-weight-bold) var(--font-size-item-title)/var(--line-height-item-title) var(--font-family-client)', '1.2', 'var(--tracking-title)', 'var(--text-transform-none)'],
|
||||||
|
'section-title': ['1rem', 'var(--font-weight-bold) var(--font-size-section-title)/var(--line-height-section-title) var(--font-family-client)', '1.25', 'var(--tracking-title)', 'var(--text-transform-none)'],
|
||||||
|
'state-title': ['1.125rem', 'var(--font-weight-bold) var(--font-size-state-title)/var(--line-height-state-title) var(--font-family-client)', '1.3', 'var(--tracking-tight)', 'var(--text-transform-none)'],
|
||||||
|
'drawer-title': ['1.375rem', 'var(--font-weight-bold) var(--font-size-drawer-title)/var(--line-height-drawer-title) var(--font-family-client)', '1.15', 'var(--tracking-tight)', 'var(--text-transform-none)'],
|
||||||
|
};
|
||||||
|
for (const [role, [size, recipe, lineHeight, tracking, transform]] of Object.entries(roleValues)) {
|
||||||
|
assert.equal(tokens.get(`--font-size-${role}`), size, role);
|
||||||
|
assert.equal(tokens.get(`--type-${role}`), recipe, role);
|
||||||
|
assert.equal(tokens.get(`--line-height-${role}`), lineHeight, role);
|
||||||
|
assert.equal(tokens.get(`--type-${role}-tracking`), tracking, role);
|
||||||
|
assert.equal(tokens.get(`--type-${role}-transform`), transform, role);
|
||||||
|
}
|
||||||
|
assert.equal(tokens.get('--type-tooltip'),
|
||||||
|
'var(--font-weight-strong) var(--font-size-control)/var(--line-height-tooltip) var(--font-family-client)');
|
||||||
|
assert.equal(tokens.get('--line-height-tooltip'), '1.35');
|
||||||
|
|
||||||
|
const allowed = new Map([
|
||||||
|
['font', new Set(['inherit', 'var(--type-icon-close)', 'var(--type-tooltip)', ...Object.keys(roleValues).map((role) => `var(--type-${role})`)])],
|
||||||
|
['font-family', new Set(['var(--font-family-client)'])],
|
||||||
|
['font-size', new Set([
|
||||||
|
'var(--font-size-brand)', 'var(--font-size-brand-tooltip)', 'var(--font-size-brand-tooltip-strong)',
|
||||||
|
'var(--font-size-icon-chevron)', 'var(--font-size-icon-delete)',
|
||||||
|
])],
|
||||||
|
['font-weight', new Set(['var(--font-weight-bold)', 'var(--font-weight-brand)', 'var(--font-weight-strong)'])],
|
||||||
|
['line-height', new Set(['var(--line-height-micro)'])],
|
||||||
|
['letter-spacing', new Set([
|
||||||
|
'inherit', 'var(--tracking-normal)', 'var(--tracking-title)',
|
||||||
|
...Object.keys(roleValues).map((role) => `var(--type-${role}-tracking)`),
|
||||||
|
'var(--type-tooltip-tracking)',
|
||||||
|
])],
|
||||||
|
['text-transform', new Set([
|
||||||
|
...Object.keys(roleValues).map((role) => `var(--type-${role}-transform)`),
|
||||||
|
'var(--type-tooltip-transform)',
|
||||||
|
])],
|
||||||
|
['font-variant-numeric', new Set(['var(--numeric-tabular)'])],
|
||||||
|
]);
|
||||||
|
for (const leaf of styleLeafPaths(root).filter((file) => !file.endsWith('/tokens.css'))) {
|
||||||
|
const stylesheet = postcss.parse(fs.readFileSync(leaf, 'utf8'), { from: leaf });
|
||||||
|
stylesheet.walkDecls((declaration) => {
|
||||||
|
if (allowed.has(declaration.prop)) {
|
||||||
|
assert.ok(allowed.get(declaration.prop).has(declaration.value),
|
||||||
|
`${path.relative(root, leaf)}:${declaration.source.start.line} ${declaration.toString()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stylesheet.walkRules((rule) => {
|
||||||
|
const font = rule.nodes.find((node) => node.type === 'decl' && node.prop === 'font');
|
||||||
|
const role = /^var\(--type-(micro|label|control|tooltip|body|data|item-title|section-title|state-title|drawer-title)\)$/.exec(font?.value || '')?.[1];
|
||||||
|
if (!role) return;
|
||||||
|
const declarations = new Map(rule.nodes
|
||||||
|
.filter((node) => node.type === 'decl')
|
||||||
|
.map((declaration) => [declaration.prop, declaration.value]));
|
||||||
|
assert.equal(declarations.get('letter-spacing'), `var(--type-${role}-tracking)`, rule.selector);
|
||||||
|
assert.equal(declarations.get('text-transform'), `var(--type-${role}-transform)`, rule.selector);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = postcss.parse(readStyleSource(root));
|
||||||
|
for (const selector of [
|
||||||
|
'.client-instructions-header h2', '.client-devices-header h2',
|
||||||
|
'.client-local-rules-header h2', '.client-profiles-header h2',
|
||||||
|
]) {
|
||||||
|
const owner = styles.nodes.find((node) => node.type === 'rule' && node.selector === selector);
|
||||||
|
assert.equal(owner?.nodes.find((node) => node.type === 'decl' && node.prop === 'font')?.value,
|
||||||
|
'var(--type-drawer-title)', selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const selector of [
|
||||||
|
'.client-duration', '.client-power-section.is-gateway .client-duration', '.client-proxy-address',
|
||||||
|
'.client-device-ip', '.client-device-traffic strong',
|
||||||
|
'.client-device-traffic-point-tooltip', '.client-device-traffic-point-tooltip time',
|
||||||
|
'.client-device-traffic-point-tooltip strong', '.client-gateway-traffic-total > strong',
|
||||||
|
'.client-gateway-traffic-speed', '.client-diagnostics-table code',
|
||||||
|
'.client-server-health', '.harbor-versions', '.harbor-version-code', '.harbor-version-number',
|
||||||
|
]) {
|
||||||
|
const owners = [];
|
||||||
|
styles.walkRules((rule) => {
|
||||||
|
if (rule.selectors.includes(selector)) owners.push(rule);
|
||||||
|
});
|
||||||
|
assert.ok(owners.length > 0, selector);
|
||||||
|
assert.ok(owners.some((owner) => owner.nodes.some((node) => node.type === 'decl'
|
||||||
|
&& node.prop === 'font-variant-numeric' && node.value === 'var(--numeric-tabular)')), selector);
|
||||||
|
for (const owner of owners.filter((rule) => rule.nodes.some((node) => node.type === 'decl' && node.prop === 'font'))) {
|
||||||
|
assert.equal(owner.nodes.find((node) => node.type === 'decl' && node.prop === 'font-variant-numeric')?.value,
|
||||||
|
'var(--numeric-tabular)', selector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
||||||
const witnesses = readStyleWitnesses(root);
|
const witnesses = readStyleWitnesses(root);
|
||||||
assert.equal(witnesses.length, 730);
|
assert.equal(witnesses.length, 730);
|
||||||
@@ -306,8 +405,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ
|
|||||||
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
||||||
|
|
||||||
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
||||||
assert.deepEqual(assets, ['index-kNiIGd5E.css']);
|
assert.deepEqual(assets, ['index-OzbtuDco.css']);
|
||||||
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
||||||
assert.equal(built.byteLength, 112296);
|
assert.equal(built.byteLength, 123218);
|
||||||
assert.equal(sha256(built), 'cbda70aff44215a69dc7c87ceaa4166ae573131bc53aaea7ddd935950b2c983f');
|
assert.equal(sha256(built), 'e462ddaffffecd18bbc0f14daebc11e6c0df149e6322e1356433d4052550389b');
|
||||||
});
|
});
|
||||||
|
|||||||
+30
-20
@@ -260,7 +260,7 @@ function dynamicClassBindings(files) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindComponentProps(definition, element, callerEnvironment, dynamicBindings) {
|
function bindComponentProps(definition, element, callerEnvironment, callerFile, dynamicBindings) {
|
||||||
const supplied = new Map();
|
const supplied = new Map();
|
||||||
for (const attribute of element.openingElement.attributes) {
|
for (const attribute of element.openingElement.attributes) {
|
||||||
if (attribute.type === 'JSXSpreadAttribute') {
|
if (attribute.type === 'JSXSpreadAttribute') {
|
||||||
@@ -277,9 +277,10 @@ function bindComponentProps(definition, element, callerEnvironment, dynamicBindi
|
|||||||
callable: ['ArrowFunctionExpression', 'FunctionExpression'].includes(node.type) && producesJsx(node),
|
callable: ['ArrowFunctionExpression', 'FunctionExpression'].includes(node.type) && producesJsx(node),
|
||||||
node,
|
node,
|
||||||
environment: callerEnvironment,
|
environment: callerEnvironment,
|
||||||
|
file: callerFile,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
supplied.set('children', { node: element.children, environment: callerEnvironment });
|
supplied.set('children', { node: element.children, environment: callerEnvironment, file: callerFile });
|
||||||
const environment = new Map(dynamicBindings.get(definition.file));
|
const environment = new Map(dynamicBindings.get(definition.file));
|
||||||
const parameter = definition.node.params?.[0];
|
const parameter = definition.node.params?.[0];
|
||||||
if (parameter?.type === 'ObjectPattern') {
|
if (parameter?.type === 'ObjectPattern') {
|
||||||
@@ -289,7 +290,7 @@ function bindComponentProps(definition, element, callerEnvironment, dynamicBindi
|
|||||||
const target = property.value?.type === 'AssignmentPattern' ? property.value.left : property.value;
|
const target = property.value?.type === 'AssignmentPattern' ? property.value.left : property.value;
|
||||||
const localName = target?.name;
|
const localName = target?.name;
|
||||||
const fallback = property.value?.type === 'AssignmentPattern'
|
const fallback = property.value?.type === 'AssignmentPattern'
|
||||||
? { node: property.value.right, environment: callerEnvironment }
|
? { node: property.value.right, environment: callerEnvironment, file: definition.file }
|
||||||
: null;
|
: null;
|
||||||
if (sourceName && localName && (supplied.get(sourceName) || fallback)) {
|
if (sourceName && localName && (supplied.get(sourceName) || fallback)) {
|
||||||
environment.set(localName, supplied.get(sourceName) || fallback);
|
environment.set(localName, supplied.get(sourceName) || fallback);
|
||||||
@@ -309,7 +310,7 @@ function producesJsx(node) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
function withLocalJsxBindings(node, environment, definitionNodes) {
|
function withLocalJsxBindings(node, environment, definitionNodes, file) {
|
||||||
const scoped = new Map(environment);
|
const scoped = new Map(environment);
|
||||||
const collect = (value) => {
|
const collect = (value) => {
|
||||||
if (!value || typeof value !== 'object') return;
|
if (!value || typeof value !== 'object') return;
|
||||||
@@ -323,6 +324,7 @@ function withLocalJsxBindings(node, environment, definitionNodes) {
|
|||||||
callable: ['ArrowFunctionExpression', 'FunctionExpression'].includes(value.init?.type),
|
callable: ['ArrowFunctionExpression', 'FunctionExpression'].includes(value.init?.type),
|
||||||
declaration: value,
|
declaration: value,
|
||||||
environment: scoped,
|
environment: scoped,
|
||||||
|
file,
|
||||||
node: value.init,
|
node: value.init,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -336,12 +338,12 @@ function withLocalJsxBindings(node, environment, definitionNodes) {
|
|||||||
return scoped;
|
return scoped;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindCallParameters(callable, argumentsList, callerEnvironment) {
|
function bindCallParameters(callable, argumentsList, callerEnvironment, callerFile) {
|
||||||
const environment = new Map(callable.environment);
|
const environment = new Map(callable.environment);
|
||||||
for (let index = 0; index < callable.node.params.length; index += 1) {
|
for (let index = 0; index < callable.node.params.length; index += 1) {
|
||||||
const parameter = callable.node.params[index];
|
const parameter = callable.node.params[index];
|
||||||
if (parameter?.type === 'Identifier' && argumentsList[index]) {
|
if (parameter?.type === 'Identifier' && argumentsList[index]) {
|
||||||
environment.set(parameter.name, { node: argumentsList[index], environment: callerEnvironment });
|
environment.set(parameter.name, { node: argumentsList[index], environment: callerEnvironment, file: callerFile });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return environment;
|
return environment;
|
||||||
@@ -361,11 +363,17 @@ export function createStyleWitnesses(sources, { entry = 'App' } = {}) {
|
|||||||
const expand = (definition, ancestors, ancestorUnknown, environment, stack) => {
|
const expand = (definition, ancestors, ancestorUnknown, environment, stack) => {
|
||||||
if (stack.includes(definition)) throw new TypeError(`Recursive JSX witness component: ${definition.node.id?.name || definition.file}`);
|
if (stack.includes(definition)) throw new TypeError(`Recursive JSX witness component: ${definition.node.id?.name || definition.file}`);
|
||||||
const nextStack = [...stack, definition];
|
const nextStack = [...stack, definition];
|
||||||
const entryEnvironment = withLocalJsxBindings(definition.node.body, environment, definitionNodes);
|
const entryEnvironment = withLocalJsxBindings(definition.node.body, environment, definitionNodes, definition.file);
|
||||||
const visit = (node, currentAncestors = ancestors, currentUnknown = ancestorUnknown, currentEnvironment = entryEnvironment) => {
|
const visit = (
|
||||||
|
node,
|
||||||
|
currentAncestors = ancestors,
|
||||||
|
currentUnknown = ancestorUnknown,
|
||||||
|
currentEnvironment = entryEnvironment,
|
||||||
|
currentFile = definition.file,
|
||||||
|
) => {
|
||||||
if (!node || typeof node !== 'object') return;
|
if (!node || typeof node !== 'object') return;
|
||||||
if (Array.isArray(node)) {
|
if (Array.isArray(node)) {
|
||||||
for (const child of node) visit(child, currentAncestors, currentUnknown, currentEnvironment);
|
for (const child of node) visit(child, currentAncestors, currentUnknown, currentEnvironment, currentFile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (definitionNodes.has(node)) return;
|
if (definitionNodes.has(node)) return;
|
||||||
@@ -378,10 +386,11 @@ export function createStyleWitnesses(sources, { entry = 'App' } = {}) {
|
|||||||
if (callable?.callable) {
|
if (callable?.callable) {
|
||||||
const callEnvironment = withLocalJsxBindings(
|
const callEnvironment = withLocalJsxBindings(
|
||||||
callable.node.body,
|
callable.node.body,
|
||||||
bindCallParameters(callable, node.arguments, currentEnvironment),
|
bindCallParameters(callable, node.arguments, currentEnvironment, currentFile),
|
||||||
definitionNodes,
|
definitionNodes,
|
||||||
|
callable.file || currentFile,
|
||||||
);
|
);
|
||||||
visit(callable.node.body, currentAncestors, currentUnknown, callEnvironment);
|
visit(callable.node.body, currentAncestors, currentUnknown, callEnvironment, callable.file || currentFile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -401,16 +410,16 @@ export function createStyleWitnesses(sources, { entry = 'App' } = {}) {
|
|||||||
if (!documentBody && !appOrBody) {
|
if (!documentBody && !appOrBody) {
|
||||||
throw new TypeError(`Unsupported JSX portal target in ${definition.file}`);
|
throw new TypeError(`Unsupported JSX portal target in ${definition.file}`);
|
||||||
}
|
}
|
||||||
visit(node.arguments[0], [], false, currentEnvironment);
|
visit(node.arguments[0], [], false, currentEnvironment, currentFile);
|
||||||
if (appOrBody) {
|
if (appOrBody) {
|
||||||
visit(node.arguments[0], [{ classes: ['app', 'client-app', 'is-gateway-app'], prefixes: [], unknown: false }], false, currentEnvironment);
|
visit(node.arguments[0], [{ classes: ['app', 'client-app', 'is-gateway-app'], prefixes: [], unknown: false }], false, currentEnvironment, currentFile);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (node.type === 'JSXExpressionContainer' && node.expression?.type === 'Identifier') {
|
if (node.type === 'JSXExpressionContainer' && node.expression?.type === 'Identifier') {
|
||||||
const binding = currentEnvironment.get(node.expression.name);
|
const binding = currentEnvironment.get(node.expression.name);
|
||||||
if (binding) {
|
if (binding) {
|
||||||
visit(binding.node, currentAncestors, currentUnknown, binding.environment);
|
visit(binding.node, currentAncestors, currentUnknown, binding.environment, binding.file || currentFile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -419,7 +428,7 @@ export function createStyleWitnesses(sources, { entry = 'App' } = {}) {
|
|||||||
const intrinsic = Boolean(name && /^[a-z]/.test(name));
|
const intrinsic = Boolean(name && /^[a-z]/.test(name));
|
||||||
if (intrinsic) {
|
if (intrinsic) {
|
||||||
const initialInfo = elementClassInfo(node.openingElement, currentEnvironment);
|
const initialInfo = elementClassInfo(node.openingElement, currentEnvironment);
|
||||||
const additions = [...imperativeByFile.get(definition.file).entries()]
|
const additions = [...imperativeByFile.get(currentFile).entries()]
|
||||||
.filter(([target]) => initialInfo.classes.includes(target))
|
.filter(([target]) => initialInfo.classes.includes(target))
|
||||||
.flatMap(([, classes]) => classes);
|
.flatMap(([, classes]) => classes);
|
||||||
const info = elementClassInfo(node.openingElement, currentEnvironment, additions);
|
const info = elementClassInfo(node.openingElement, currentEnvironment, additions);
|
||||||
@@ -432,26 +441,26 @@ export function createStyleWitnesses(sources, { entry = 'App' } = {}) {
|
|||||||
tag: name,
|
tag: name,
|
||||||
unknown: info.unknown,
|
unknown: info.unknown,
|
||||||
});
|
});
|
||||||
visit(node.children, [...currentAncestors, info], currentUnknown, currentEnvironment);
|
visit(node.children, [...currentAncestors, info], currentUnknown, currentEnvironment, currentFile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (TRANSPARENT_JSX_COMPONENTS.has(name)) {
|
if (TRANSPARENT_JSX_COMPONENTS.has(name)) {
|
||||||
visit(node.children, currentAncestors, currentUnknown, currentEnvironment);
|
visit(node.children, currentAncestors, currentUnknown, currentEnvironment, currentFile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const target = definitions.get(name);
|
const target = definitions.get(name);
|
||||||
if (!target) throw new TypeError(`Unresolved JSX witness component: ${name} in ${definition.file}`);
|
if (!target) throw new TypeError(`Unresolved JSX witness component: ${name} in ${definition.file}`);
|
||||||
const targetEnvironment = bindComponentProps(target, node, currentEnvironment, dynamicByFile);
|
const targetEnvironment = bindComponentProps(target, node, currentEnvironment, currentFile, dynamicByFile);
|
||||||
expand(target, currentAncestors, currentUnknown, targetEnvironment, nextStack);
|
expand(target, currentAncestors, currentUnknown, targetEnvironment, nextStack);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (node.type === 'JSXFragment') {
|
if (node.type === 'JSXFragment') {
|
||||||
visit(node.children, currentAncestors, currentUnknown, currentEnvironment);
|
visit(node.children, currentAncestors, currentUnknown, currentEnvironment, currentFile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const [key, child] of Object.entries(node)) {
|
for (const [key, child] of Object.entries(node)) {
|
||||||
if (['loc', 'start', 'end', 'extra', 'comments', 'tokens', 'errors'].includes(key)) continue;
|
if (['loc', 'start', 'end', 'extra', 'comments', 'tokens', 'errors'].includes(key)) continue;
|
||||||
if (child && typeof child === 'object') visit(child, currentAncestors, currentUnknown, currentEnvironment);
|
if (child && typeof child === 'object') visit(child, currentAncestors, currentUnknown, currentEnvironment, currentFile);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
visit(definition.node.body, ancestors, ancestorUnknown, entryEnvironment);
|
visit(definition.node.body, ancestors, ancestorUnknown, entryEnvironment);
|
||||||
@@ -521,6 +530,7 @@ const TRANSPARENT_JSX_COMPONENTS = new Set(['React.Fragment']);
|
|||||||
const hash = (value) => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
const hash = (value) => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
||||||
|
|
||||||
function propertyFamily(property) {
|
function propertyFamily(property) {
|
||||||
|
if (property.startsWith('--')) return property;
|
||||||
if (!OBSERVED_PROPERTIES.has(property)) {
|
if (!OBSERVED_PROPERTIES.has(property)) {
|
||||||
throw new TypeError(`Unsupported CSS property in cascade proof: ${property}`);
|
throw new TypeError(`Unsupported CSS property in cascade proof: ${property}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
const root = path.resolve(import.meta.dirname, '../..');
|
||||||
|
const webRoot = path.join(root, 'src/web');
|
||||||
|
const sourceFiles = fs.readdirSync(webRoot, { recursive: true, withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isFile() && entry.name.endsWith('.tsx'))
|
||||||
|
.map((entry) => path.join(entry.parentPath, entry.name));
|
||||||
|
const sources = sourceFiles.map((file) => ({ file, source: fs.readFileSync(file, 'utf8') }));
|
||||||
|
const ui = (name) => fs.readFileSync(path.join(webRoot, 'ui', `${name}.tsx`), 'utf8');
|
||||||
|
|
||||||
|
test('shared client primitives own repeated markup without feature dependencies', () => {
|
||||||
|
const tooltip = ui('Tooltip');
|
||||||
|
const copyButton = ui('CopyButton');
|
||||||
|
const railAction = ui('RailAction');
|
||||||
|
const drawer = ui('Drawer');
|
||||||
|
|
||||||
|
for (const source of [tooltip, copyButton, railAction, drawer]) {
|
||||||
|
assert.doesNotMatch(source, /features|api|state\//);
|
||||||
|
}
|
||||||
|
assert.match(tooltip, /className="client-tooltip"[\s\S]*role="tooltip"/);
|
||||||
|
assert.match(copyButton, /client-copy-label[\s\S]*client-copy-feedback/);
|
||||||
|
assert.match(railAction, /aria-expanded=\{open\}[\s\S]*aria-controls=\{controls\}/);
|
||||||
|
assert.match(drawer, /<aside[\s\S]*aria-hidden=\{!open\}[\s\S]*inert=\{!open[\s\S]*client-drawer-close/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all repeated client primitive consumers use the shared owners', () => {
|
||||||
|
const production = sources
|
||||||
|
.filter(({ file }) => !file.includes(`${path.sep}ui${path.sep}`))
|
||||||
|
.map(({ source }) => source)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
assert.equal((production.match(/<Tooltip\b/g) || []).length, 9);
|
||||||
|
assert.equal((production.match(/<CopyButton\b/g) || []).length, 2);
|
||||||
|
assert.equal((production.match(/<RailAction\b/g) || []).length, 5);
|
||||||
|
assert.equal((production.match(/<Drawer\b/g) || []).length, 5);
|
||||||
|
assert.doesNotMatch(production, /className="client-tooltip"|className="client-copy-label"|className="client-drawer-close"/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user