Refine rail drawer switching and toggle animations
This commit is contained in:
@@ -445,6 +445,7 @@ export function ClientOverviewPage({
|
||||
const [showIntro, setShowIntro] = useState(true);
|
||||
const [copyFeedback, setCopyFeedback] = useState<CopyFeedbackMap>({});
|
||||
const [copyAnnouncement, setCopyAnnouncement] = useState<CopyAnnouncement>({ text: '', cycle: 0 });
|
||||
const [drawerSwitchTarget, setDrawerSwitchTarget] = useState<DrawerKey | null>(null);
|
||||
const copyTimersRef = useRef<Partial<Record<CopyKind, ReturnType<typeof setTimeout>>>>({});
|
||||
const copyAttemptsRef = useRef<Partial<Record<CopyKind, object>>>({});
|
||||
const drawerSwitchRef = useRef<{
|
||||
@@ -567,6 +568,9 @@ export function ClientOverviewPage({
|
||||
const drawerOrder = isGateway
|
||||
? DRAWER_ORDER
|
||||
: DRAWER_ORDER.filter((drawer) => drawer !== 'devices');
|
||||
const activeRailDrawer = drawerSwitchTarget && drawerControls[drawerSwitchTarget].isOpen
|
||||
? drawerSwitchTarget
|
||||
: drawerOrder.find((drawer) => drawerControls[drawer].isOpen) || null;
|
||||
|
||||
useEffect(() => {
|
||||
setNow(Date.now());
|
||||
@@ -673,11 +677,13 @@ export function ClientOverviewPage({
|
||||
return;
|
||||
}
|
||||
|
||||
setDrawerSwitchTarget(target);
|
||||
flushSync(() => toControl.show());
|
||||
const from = fromControl.panelRef.current;
|
||||
const to = toControl.panelRef.current;
|
||||
if (!from || !to || typeof from.animate !== 'function' || typeof to.animate !== 'function') {
|
||||
fromControl.close();
|
||||
setDrawerSwitchTarget(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -707,7 +713,10 @@ export function ClientOverviewPage({
|
||||
const finish = () => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
flushSync(() => fromControl.close());
|
||||
flushSync(() => {
|
||||
fromControl.close();
|
||||
setDrawerSwitchTarget(null);
|
||||
});
|
||||
cancel();
|
||||
if (drawerSwitchRef.current?.target === target) drawerSwitchRef.current = null;
|
||||
};
|
||||
@@ -745,22 +754,27 @@ export function ClientOverviewPage({
|
||||
>
|
||||
<SubscriptionToggle
|
||||
feature={subscriptionFeature}
|
||||
open={activeRailDrawer === 'subscription'}
|
||||
onToggle={() => switchDrawer('subscription')}
|
||||
/>
|
||||
<InstructionsToggle
|
||||
feature={instructionsFeature}
|
||||
open={activeRailDrawer === 'instructions'}
|
||||
onToggle={() => switchDrawer('instructions')}
|
||||
/>
|
||||
{isGateway && <DevicesToggle
|
||||
feature={devicesFeature}
|
||||
open={activeRailDrawer === 'devices'}
|
||||
onToggle={() => switchDrawer('devices')}
|
||||
/>}
|
||||
<DiagnosticsToggle
|
||||
feature={diagnosticsFeature}
|
||||
open={activeRailDrawer === 'diagnostics'}
|
||||
onToggle={() => switchDrawer('diagnostics')}
|
||||
/>
|
||||
<RoutingToggle
|
||||
feature={routingFeature}
|
||||
open={activeRailDrawer === 'routing'}
|
||||
gatewayDirect={gatewayDirect}
|
||||
isGateway={isGateway}
|
||||
hasSubscription={hasSubscription}
|
||||
|
||||
@@ -224,20 +224,30 @@ export function useDevicesFeature({
|
||||
|
||||
export type DevicesFeature = ReturnType<typeof useDevicesFeature>;
|
||||
|
||||
export function DevicesToggle({ feature, onToggle }: { feature: DevicesFeature; onToggle: () => void }) {
|
||||
export function DevicesToggle({
|
||||
feature,
|
||||
open,
|
||||
onToggle,
|
||||
}: {
|
||||
feature: DevicesFeature;
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
return <RailAction
|
||||
buttonRef={feature.toggleRef}
|
||||
className="client-instructions-toggle client-devices-toggle"
|
||||
open={feature.isOpen}
|
||||
open={open}
|
||||
controls="client-devices"
|
||||
ariaLabel={feature.isOpen ? 'Закрыть устройства' : 'Устройства Gateway'}
|
||||
ariaLabel={open ? 'Закрыть устройства' : 'Устройства Gateway'}
|
||||
label="Устройства"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect className="client-rail-device-primary" x="3.5" y="5" width="7" height="10" 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="M7 15v4h3.5M17 15v4h-3.5" />
|
||||
<path className="client-rail-device-bridge" d="M10.5 19h3" />
|
||||
<path className="client-rail-device-pulse" pathLength="1" d="M7 19h10" />
|
||||
</svg>
|
||||
</RailAction>;
|
||||
}
|
||||
|
||||
@@ -43,17 +43,19 @@ export type DiagnosticsFeature = ReturnType<typeof useDiagnosticsFeature>;
|
||||
|
||||
export function DiagnosticsToggle({
|
||||
feature,
|
||||
open,
|
||||
onToggle,
|
||||
}: {
|
||||
feature: DiagnosticsFeature;
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
return <RailAction
|
||||
buttonRef={feature.toggleRef}
|
||||
className="client-instructions-toggle client-diagnostics-toggle"
|
||||
open={feature.isOpen}
|
||||
open={open}
|
||||
controls="client-diagnostics"
|
||||
ariaLabel={feature.isOpen ? 'Закрыть диагностику' : 'Проверить маршруты'}
|
||||
ariaLabel={open ? 'Закрыть диагностику' : 'Проверить маршруты'}
|
||||
label="Диагностика"
|
||||
onClick={onToggle}
|
||||
>
|
||||
|
||||
@@ -232,23 +232,26 @@ export type InstructionsFeature = ReturnType<typeof useInstructionsFeature>;
|
||||
|
||||
export function InstructionsToggle({
|
||||
feature,
|
||||
open,
|
||||
onToggle,
|
||||
}: {
|
||||
feature: InstructionsFeature;
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
return <RailAction
|
||||
buttonRef={feature.toggleRef}
|
||||
className="client-instructions-toggle"
|
||||
open={feature.isOpen}
|
||||
open={open}
|
||||
controls="client-instructions"
|
||||
ariaLabel={feature.isOpen ? 'Закрыть инструкции' : 'Как использовать'}
|
||||
ariaLabel={open ? 'Закрыть инструкции' : 'Как использовать'}
|
||||
label="Как использовать"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle className="client-rail-info-ring" cx="12" cy="12" r="8.5" pathLength="1" />
|
||||
<path d="M12 11v5M12 8h.01" />
|
||||
<path className="client-rail-book-page is-left" d="M12 7.5C10.2 6.1 7.7 5.7 5 6v11c2.8-.3 5.2.3 7 1.7z" />
|
||||
<path className="client-rail-book-page is-right" d="M12 7.5c1.8-1.4 4.3-1.8 7-1.5v11c-2.8-.3-5.2.3-7 1.7z" />
|
||||
<path className="client-rail-book-spine" d="M12 7.5v11.2" />
|
||||
</svg>
|
||||
</RailAction>;
|
||||
}
|
||||
|
||||
@@ -381,12 +381,14 @@ function RuleTypePicker({ value, ruleKey, index, disabled, onChange }: RuleTypeP
|
||||
|
||||
export function RoutingToggle({
|
||||
feature,
|
||||
open,
|
||||
gatewayDirect,
|
||||
isGateway,
|
||||
hasSubscription,
|
||||
onOpen,
|
||||
}: {
|
||||
feature: RoutingFeature;
|
||||
open: boolean;
|
||||
gatewayDirect: boolean;
|
||||
isGateway: boolean;
|
||||
hasSubscription: boolean;
|
||||
@@ -397,25 +399,25 @@ export function RoutingToggle({
|
||||
<RailAction
|
||||
buttonRef={feature.toggleRef}
|
||||
className={`client-local-rules-toggle${feature.pendingRestart ? ' has-pending' : ''}`}
|
||||
open={feature.isOpen}
|
||||
open={open}
|
||||
controls="client-local-rules"
|
||||
disabled={disabled}
|
||||
ariaLabel={disabled
|
||||
? hasSubscription
|
||||
? 'Локальные правила недоступны: сейчас работают правила Harbor Gateway'
|
||||
: 'Локальные правила недоступны: сначала добавьте подписку'
|
||||
: feature.isOpen ? 'Закрыть локальные правила' : 'Настроить локальные правила'}
|
||||
: open ? 'Закрыть локальные правила' : 'Настроить локальные правила'}
|
||||
label={disabled
|
||||
? hasSubscription
|
||||
? 'Локальные правила недоступны: сейчас работают правила Gateway'
|
||||
: 'Сначала добавьте подписку'
|
||||
: feature.pendingRestart ? 'Правила ждут перезапуска' : 'Локальные правила'}
|
||||
onClick={() => feature.isOpen ? feature.requestClose() : onOpen()}
|
||||
onClick={() => open ? feature.requestClose() : onOpen()}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4 7h9M17 7h3M4 17h3M11 17h9" />
|
||||
<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" />
|
||||
<path className="client-rail-rule-track" d="M4 7h16M4 17h16" />
|
||||
<circle className="client-rail-rule-knob is-top" cx="12" cy="7" r="2" />
|
||||
<circle className="client-rail-rule-knob is-bottom" cx="12" cy="17" r="2" />
|
||||
</svg>
|
||||
</RailAction>
|
||||
);
|
||||
|
||||
@@ -301,22 +301,28 @@ type SubscriptionFeatureController = ReturnType<typeof useSubscriptionFeature>;
|
||||
|
||||
export function SubscriptionToggle({
|
||||
feature,
|
||||
open,
|
||||
onToggle,
|
||||
}: {
|
||||
feature: SubscriptionFeatureController;
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
return <RailAction
|
||||
buttonRef={feature.toggleRef}
|
||||
className="client-instructions-toggle client-subscription-toggle"
|
||||
open={feature.open}
|
||||
open={open}
|
||||
controls="client-subscription-drawer"
|
||||
ariaLabel={feature.open ? 'Закрыть подписки' : 'Управление подписками'}
|
||||
ariaLabel={open ? 'Закрыть подписки' : 'Управление подписками'}
|
||||
label="Подписки"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M5 5h14v14H5zM8 9h8M8 13h5" />
|
||||
<rect className="client-rail-subscription-back" x="6" y="4.5" width="13" height="15" rx="1.5" />
|
||||
<g className="client-rail-subscription-front">
|
||||
<rect className="client-rail-subscription-card" x="4" y="6.5" width="13" height="13" rx="1.5" />
|
||||
<path className="client-rail-subscription-lines" d="M7.5 10.5h6M7.5 14h4" />
|
||||
</g>
|
||||
</svg>
|
||||
</RailAction>;
|
||||
}
|
||||
|
||||
+115
-43
@@ -68,80 +68,152 @@
|
||||
filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-accent) 52%, transparent));
|
||||
}
|
||||
|
||||
.client-rail-subscription-back,
|
||||
.client-rail-subscription-front,
|
||||
.client-rail-subscription-lines,
|
||||
.client-rail-book-page,
|
||||
.client-rail-book-spine,
|
||||
.client-rail-device-primary,
|
||||
.client-rail-device-secondary,
|
||||
.client-rail-device-link,
|
||||
.client-rail-device-bridge,
|
||||
.client-rail-device-pulse,
|
||||
.client-rail-diagnostics-base,
|
||||
.client-rail-diagnostics-pulse,
|
||||
.client-rail-rule-knob {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
transition: transform 620ms cubic-bezier(0.16, 1, 0.3, 1), opacity 360ms ease, stroke-dashoffset 620ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-rail-info-ring {
|
||||
stroke-dasharray: 1;
|
||||
.client-rail-action .client-rail-subscription-card,
|
||||
.client-rail-action .client-rail-rule-knob {
|
||||
fill: var(--client-bg);
|
||||
}
|
||||
|
||||
.client-rail-subscription-back {
|
||||
transform: translate(-1.4px, 1.4px);
|
||||
}
|
||||
|
||||
.client-rail-subscription-front {
|
||||
transform: translateX(1px);
|
||||
}
|
||||
|
||||
.client-rail-subscription-lines {
|
||||
opacity: 0.52;
|
||||
transform: scaleX(0.68);
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
.client-subscription-toggle.is-open .client-rail-subscription-back {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.client-subscription-toggle.is-open .client-rail-subscription-front {
|
||||
transform: translate(-1px, 0.6px);
|
||||
}
|
||||
|
||||
.client-subscription-toggle.is-open .client-rail-subscription-lines {
|
||||
opacity: 1;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.client-rail-book-page.is-left {
|
||||
transform: translateX(1.7px) scaleX(0.58);
|
||||
transform-origin: right center;
|
||||
}
|
||||
|
||||
.client-rail-book-page.is-right {
|
||||
transform: translateX(-1.7px) scaleX(0.58);
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
.client-rail-book-spine {
|
||||
opacity: 0.55;
|
||||
transform: scaleY(0.74);
|
||||
}
|
||||
|
||||
.client-instructions-toggle.is-open .client-rail-book-page,
|
||||
.client-instructions-toggle.is-open .client-rail-book-spine {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
|
||||
.client-rail-device-primary {
|
||||
transform: translate(-0.9px, -1.8px);
|
||||
}
|
||||
|
||||
.client-rail-device-secondary {
|
||||
transform: translate(0.9px, -1.8px);
|
||||
}
|
||||
|
||||
.client-rail-device-link {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.client-rail-device-bridge {
|
||||
opacity: 0;
|
||||
transform: scaleX(0);
|
||||
}
|
||||
|
||||
.client-rail-device-pulse,
|
||||
.client-rail-diagnostics-pulse {
|
||||
stroke-width: 2.4;
|
||||
opacity: 0;
|
||||
stroke-dasharray: 0.16 0.84;
|
||||
}
|
||||
|
||||
.client-instructions-toggle.is-open:not(.client-devices-toggle):not(.client-diagnostics-toggle) .client-rail-info-ring {
|
||||
animation: client-rail-info-refill 680ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-devices-toggle.is-open .client-rail-device-primary {
|
||||
animation: client-rail-device-left 680ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-devices-toggle.is-open .client-rail-device-primary,
|
||||
.client-devices-toggle.is-open .client-rail-device-secondary {
|
||||
animation: client-rail-device-right 680ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.client-devices-toggle.is-open .client-rail-device-link {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.client-devices-toggle.is-open .client-rail-device-bridge {
|
||||
opacity: 1;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.client-devices-toggle.is-open .client-rail-device-pulse {
|
||||
animation: client-rail-device-signal 760ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-rail-diagnostics-base,
|
||||
.client-rail-diagnostics-pulse {
|
||||
vector-effect: non-scaling-stroke;
|
||||
transform: scaleY(0.22);
|
||||
}
|
||||
|
||||
.client-diagnostics-toggle.is-open .client-rail-diagnostics-base,
|
||||
.client-diagnostics-toggle.is-open .client-rail-diagnostics-pulse {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.client-diagnostics-toggle.is-open .client-rail-diagnostics-pulse {
|
||||
animation: client-rail-diagnostics-pulse 760ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
animation: client-rail-diagnostics-scan 760ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-local-rules-toggle.is-open .client-rail-rule-knob.is-top {
|
||||
animation: client-rail-rule-top 680ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.client-local-rules-toggle.is-open .client-rail-rule-knob.is-bottom {
|
||||
animation: client-rail-rule-bottom 680ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
|
||||
@keyframes client-rail-info-refill {
|
||||
from { opacity: 0.28; stroke-dashoffset: 1; }
|
||||
to { opacity: 1; stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
@keyframes client-rail-device-left {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
38%, 58% { transform: translate(-0.6px, -2.25px); }
|
||||
82% { transform: translate(0.15px, 0.35px); }
|
||||
}
|
||||
|
||||
@keyframes client-rail-device-right {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
38%, 58% { transform: translate(0.6px, -2.25px); }
|
||||
82% { transform: translate(-0.15px, 0.35px); }
|
||||
}
|
||||
|
||||
@keyframes client-rail-diagnostics-pulse {
|
||||
@keyframes client-rail-device-signal {
|
||||
0% { opacity: 0; stroke-dashoffset: 1; }
|
||||
16%, 84% { opacity: 1; }
|
||||
18%, 78% { opacity: 1; }
|
||||
100% { opacity: 0; stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
@keyframes client-rail-rule-top {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
42% { transform: translateX(-3px); }
|
||||
72% { transform: translateX(1px); }
|
||||
}
|
||||
|
||||
@keyframes client-rail-rule-bottom {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
42% { transform: translateX(3px); }
|
||||
72% { transform: translateX(-1px); }
|
||||
@keyframes client-rail-diagnostics-scan {
|
||||
0% { opacity: 0; stroke-dashoffset: 1; }
|
||||
16%, 84% { opacity: 1; }
|
||||
100% { opacity: 0; stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
.client-local-rules-toggle:disabled {
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
.client-drawer-close,
|
||||
.client-rail-action,
|
||||
.client-rail-action svg,
|
||||
.client-rail-action g,
|
||||
.client-rail-action circle,
|
||||
.client-rail-action path,
|
||||
.client-rail-action rect,
|
||||
|
||||
Reference in New Issue
Block a user