Improve routing rule drag handling and controls
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
export const HARBOR_VERSIONS = Object.freeze({
|
||||
macClient: '0.26.2',
|
||||
gatewayClient: '0.27.2',
|
||||
macClient: '0.26.3',
|
||||
gatewayClient: '0.27.3',
|
||||
gatewayBackend: '0.27.0',
|
||||
});
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ interface RuleDragSession extends RuleReorderLifecycle {
|
||||
pointerOffsetY: number;
|
||||
translateY: number;
|
||||
raf: number | null;
|
||||
capture: HTMLElement;
|
||||
handle: HTMLButtonElement;
|
||||
}
|
||||
|
||||
@@ -447,9 +448,10 @@ export function useRoutingFeature({
|
||||
const lifecycle = beginRuleReorder(dragRef.current, ruleKey, 'pointer');
|
||||
if (!lifecycle) return;
|
||||
const row = event.currentTarget.closest<HTMLElement>('[data-rule-key]');
|
||||
if (!row) return;
|
||||
const capture = event.currentTarget.closest<HTMLElement>('.client-local-rules-list');
|
||||
if (!row || !capture) return;
|
||||
const rect = row.getBoundingClientRect();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
capture.setPointerCapture(event.pointerId);
|
||||
dragRef.current = {
|
||||
...lifecycle,
|
||||
originRules: [...rulesRef.current],
|
||||
@@ -459,11 +461,12 @@ export function useRoutingFeature({
|
||||
pointerOffsetY: event.clientY - (rect.top + rect.height / 2),
|
||||
translateY: 0,
|
||||
raf: null,
|
||||
capture,
|
||||
handle: event.currentTarget,
|
||||
};
|
||||
}
|
||||
|
||||
function movePointerReorder(event: ReactPointerEvent<HTMLButtonElement>) {
|
||||
function movePointerReorder(event: ReactPointerEvent<HTMLElement>) {
|
||||
const session = dragRef.current;
|
||||
if (!session || session.pointerId !== event.pointerId) return;
|
||||
session.latestY = event.clientY;
|
||||
@@ -514,9 +517,9 @@ export function useRoutingFeature({
|
||||
if (
|
||||
outcome.releasePointerCapture
|
||||
&& session.pointerId !== null
|
||||
&& session.handle.hasPointerCapture(session.pointerId)
|
||||
&& session.capture.hasPointerCapture(session.pointerId)
|
||||
) {
|
||||
session.handle.releasePointerCapture(session.pointerId);
|
||||
session.capture.releasePointerCapture(session.pointerId);
|
||||
}
|
||||
if (session.lifted) {
|
||||
if (reason === 'drop' && session.input === 'pointer') suppressHandleClickRef.current = true;
|
||||
@@ -536,17 +539,17 @@ export function useRoutingFeature({
|
||||
return finishReorder('cancel', announce);
|
||||
}
|
||||
|
||||
function finishPointerReorder(event: ReactPointerEvent<HTMLButtonElement>) {
|
||||
function finishPointerReorder(event: ReactPointerEvent<HTMLElement>) {
|
||||
if (dragRef.current?.pointerId !== event.pointerId) return;
|
||||
dropReorder();
|
||||
}
|
||||
|
||||
function cancelPointerReorder(event: ReactPointerEvent<HTMLButtonElement>) {
|
||||
function cancelPointerReorder(event: ReactPointerEvent<HTMLElement>) {
|
||||
if (dragRef.current?.pointerId !== event.pointerId) return;
|
||||
cancelReorder();
|
||||
}
|
||||
|
||||
function losePointerReorder(event: ReactPointerEvent<HTMLButtonElement>) {
|
||||
function losePointerReorder(event: ReactPointerEvent<HTMLElement>) {
|
||||
if (dragRef.current?.pointerId !== event.pointerId) return;
|
||||
finishReorder('lost-capture');
|
||||
}
|
||||
@@ -569,6 +572,7 @@ export function useRoutingFeature({
|
||||
pointerOffsetY: 0,
|
||||
translateY: 0,
|
||||
raf: null,
|
||||
capture: handle,
|
||||
handle,
|
||||
};
|
||||
setLiftedKey(ruleKey);
|
||||
@@ -929,6 +933,10 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
||||
className={`client-local-rules-list${draftRules.length > 1 ? ' has-order-flow' : ''}`}
|
||||
role="list"
|
||||
aria-label="Правила применяются сверху вниз"
|
||||
onPointerMove={feature.movePointerReorder}
|
||||
onPointerUp={feature.finishPointerReorder}
|
||||
onPointerCancel={feature.cancelPointerReorder}
|
||||
onLostPointerCapture={feature.losePointerReorder}
|
||||
>
|
||||
{feature.rules.map((rule, index) => {
|
||||
const [status, statusLabel] = localRuleStatus(
|
||||
@@ -959,18 +967,19 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
||||
aria-pressed={lifted}
|
||||
disabled={handleDisabled}
|
||||
onPointerDown={(event) => feature.startPointerReorder(event, rule._key)}
|
||||
onPointerMove={feature.movePointerReorder}
|
||||
onPointerUp={feature.finishPointerReorder}
|
||||
onPointerCancel={feature.cancelPointerReorder}
|
||||
onLostPointerCapture={feature.losePointerReorder}
|
||||
onKeyDown={(event) => feature.handleReorderKey(event, rule._key)}
|
||||
onClick={(event) => feature.handleReorderClick(event, rule._key)}
|
||||
onBlur={(event) => feature.handleReorderBlur(event, rule._key)}
|
||||
>
|
||||
<svg viewBox="0 0 12 28" aria-hidden="true">
|
||||
<circle cx="6" cy="6" r="1.6" />
|
||||
<circle cx="6" cy="14" r="1.6" />
|
||||
<circle cx="6" cy="22" r="1.6" />
|
||||
<svg viewBox="0 0 16 28" aria-hidden="true">
|
||||
<circle cx="5" cy="5" r="1.25" />
|
||||
<circle cx="11" cy="5" r="1.25" />
|
||||
<circle cx="5" cy="11" r="1.25" />
|
||||
<circle cx="11" cy="11" r="1.25" />
|
||||
<circle cx="5" cy="17" r="1.25" />
|
||||
<circle cx="11" cy="17" r="1.25" />
|
||||
<circle cx="5" cy="23" r="1.25" />
|
||||
<circle cx="11" cy="23" r="1.25" />
|
||||
</svg>
|
||||
</button>
|
||||
<span className="client-local-rule-enabled-wrap client-tooltip-anchor">
|
||||
@@ -984,11 +993,15 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
||||
onClick={() => feature.change(index, 'enabled', !rule.enabled)}
|
||||
>
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true">
|
||||
<rect className="client-rule-switch-track" x="2" y="6" width="16" height="8" rx="4" />
|
||||
<circle className="client-rule-switch-thumb" cx="6" cy="10" r="2.5" />
|
||||
<circle cx="10" cy="10" r="6" />
|
||||
<path className="client-rule-check" d="m6.8 10.1 2.1 2.2 4.5-5" />
|
||||
</svg>
|
||||
</button>
|
||||
<Tooltip>{rule.enabled ? 'Отключить правило' : 'Включить правило'}</Tooltip>
|
||||
<Tooltip>
|
||||
{rule.enabled
|
||||
? 'Отключить: правило перестанет участвовать в маршрутизации'
|
||||
: 'Включить: правило снова будет участвовать в маршрутизации'}
|
||||
</Tooltip>
|
||||
</span>
|
||||
<RuleTypePicker
|
||||
value={rule.type}
|
||||
|
||||
@@ -111,7 +111,8 @@
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
padding: 4px 0;
|
||||
background: linear-gradient(90deg, color-mix(in oklch, var(--client-rule-route-color) 8%, transparent), transparent 58%);
|
||||
border-radius: 10px;
|
||||
background: color-mix(in oklch, var(--client-rule-route-color) 6%, transparent);
|
||||
animation: client-row-enter 560ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
transition: background 320ms ease, box-shadow 180ms ease, opacity 260ms ease, filter 360ms ease;
|
||||
}
|
||||
@@ -120,21 +121,6 @@
|
||||
--client-rule-route-color: light-dark(oklch(0.52 0.11 240), oklch(0.76 0.1 240));
|
||||
}
|
||||
|
||||
.client-local-rule::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
border-radius: 999px;
|
||||
background: var(--client-rule-route-color);
|
||||
box-shadow: 0 0 7px color-mix(in oklch, var(--client-rule-route-color) 44%, transparent);
|
||||
opacity: 0.78;
|
||||
pointer-events: none;
|
||||
transition: background 320ms ease, box-shadow 320ms ease, opacity 260ms ease;
|
||||
}
|
||||
|
||||
.client-local-rule + .client-local-rule::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -208,15 +194,10 @@
|
||||
filter: saturate(0);
|
||||
}
|
||||
|
||||
.client-local-rule.is-disabled::after {
|
||||
opacity: 0.22;
|
||||
filter: saturate(0);
|
||||
}
|
||||
|
||||
.client-local-rule.is-dragging {
|
||||
z-index: 6;
|
||||
animation: none;
|
||||
background: linear-gradient(90deg, color-mix(in oklch, var(--client-rule-route-color) 13%, transparent), transparent 72%);
|
||||
background: color-mix(in oklch, var(--client-rule-route-color) 11%, transparent);
|
||||
box-shadow: 0 10px 28px color-mix(in oklch, var(--client-rule-route-color) 16%, transparent);
|
||||
transform: translateY(var(--client-rule-drag-y));
|
||||
}
|
||||
@@ -260,10 +241,14 @@
|
||||
}
|
||||
|
||||
.client-rule-handle svg {
|
||||
width: 12px;
|
||||
height: 28px;
|
||||
width: 18px;
|
||||
height: 32px;
|
||||
overflow: visible;
|
||||
padding: 2px 1px;
|
||||
border-radius: 7px;
|
||||
background: color-mix(in oklch, currentColor 6%, transparent);
|
||||
fill: currentColor;
|
||||
transition: background 220ms ease, transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-rule-handle:hover:not(:disabled),
|
||||
@@ -278,32 +263,39 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.client-rule-handle:hover:not(:disabled) svg,
|
||||
.client-rule-handle:focus-visible svg,
|
||||
.client-rule-handle[aria-pressed='true'] svg {
|
||||
background: color-mix(in oklch, currentColor 12%, transparent);
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
.client-rule-handle:disabled {
|
||||
opacity: 0.28;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.client-local-rule-enabled svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
overflow: visible;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
transition: color 260ms ease, filter 360ms ease, transform 420ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-rule-switch-track {
|
||||
fill: color-mix(in oklch, var(--client-muted) 16%, transparent);
|
||||
stroke: currentColor;
|
||||
stroke-width: 1;
|
||||
transition: fill 260ms ease, stroke 260ms ease;
|
||||
.client-local-rule-enabled circle {
|
||||
stroke-width: 1.4;
|
||||
transition: fill 320ms ease, stroke 260ms ease;
|
||||
}
|
||||
|
||||
.client-rule-switch-thumb {
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
transition: transform 320ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
.client-rule-check {
|
||||
stroke-width: 1.8;
|
||||
stroke-dasharray: 12;
|
||||
stroke-dashoffset: 12;
|
||||
transition: opacity 160ms ease, stroke-dashoffset 360ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-local-rule-enabled[aria-checked='true'] {
|
||||
@@ -311,15 +303,16 @@
|
||||
}
|
||||
|
||||
.client-local-rule-enabled[aria-checked='true'] svg {
|
||||
filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-rule-route-color) 42%, transparent));
|
||||
filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-rule-route-color) 48%, transparent));
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.client-local-rule-enabled[aria-checked='true'] .client-rule-switch-track {
|
||||
fill: color-mix(in oklch, var(--client-rule-route-color) 20%, transparent);
|
||||
.client-local-rule-enabled[aria-checked='true'] circle {
|
||||
fill: color-mix(in oklch, var(--client-rule-route-color) 12%, transparent);
|
||||
}
|
||||
|
||||
.client-local-rule-enabled[aria-checked='true'] .client-rule-switch-thumb {
|
||||
transform: translateX(8px);
|
||||
.client-local-rule-enabled[aria-checked='true'] .client-rule-check {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
.client-rule-type {
|
||||
|
||||
@@ -287,11 +287,12 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.client-deletable-row.is-removing > :not(.client-delete-strike) {
|
||||
.client-deletable-row.is-removing > :not(.client-delete-strike):not(.client-local-rule-enabled-wrap) {
|
||||
animation: client-delete-content-dim 820ms ease-out both;
|
||||
}
|
||||
|
||||
.client-deletable-row.is-removing .client-rule-switch-thumb {
|
||||
.client-deletable-row.is-removing .client-local-rule-enabled circle,
|
||||
.client-deletable-row.is-removing .client-rule-check {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -301,15 +302,15 @@
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--client-muted);
|
||||
color: oklch(0.62 0.16 28);
|
||||
font-size: var(--font-size-icon-delete);
|
||||
cursor: pointer;
|
||||
transition: color 180ms ease, filter 260ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-row-delete:hover {
|
||||
color: oklch(0.68 0.15 28);
|
||||
filter: drop-shadow(0 0 7px oklch(0.68 0.15 28 / 0.42));
|
||||
color: oklch(0.7 0.18 28);
|
||||
filter: drop-shadow(0 0 7px oklch(0.7 0.18 28 / 0.42));
|
||||
}
|
||||
|
||||
.client-row-delete:hover:not(:has(svg)) {
|
||||
@@ -341,7 +342,6 @@
|
||||
.client-local-rule-enabled:focus-visible,
|
||||
.client-rule-type-trigger:focus-visible,
|
||||
.client-rule-outbound button:focus-visible,
|
||||
.client-row-delete:focus-visible,
|
||||
.client-row-add:focus-visible,
|
||||
.client-local-rules-save:focus-visible,
|
||||
.client-local-rules-actions button:focus-visible {
|
||||
@@ -350,6 +350,12 @@
|
||||
text-shadow: 0 0 10px color-mix(in oklch, var(--client-accent) 48%, transparent);
|
||||
}
|
||||
|
||||
.client-row-delete:focus-visible {
|
||||
outline: 0;
|
||||
color: oklch(0.7 0.18 28);
|
||||
filter: drop-shadow(0 0 7px oklch(0.7 0.18 28 / 0.42));
|
||||
}
|
||||
|
||||
.client-row-add {
|
||||
width: fit-content;
|
||||
padding: 7px 0;
|
||||
|
||||
@@ -122,11 +122,11 @@
|
||||
}
|
||||
|
||||
.client-local-rule,
|
||||
.client-local-rule::after,
|
||||
.client-rule-handle,
|
||||
.client-rule-handle svg,
|
||||
.client-local-rule-enabled svg,
|
||||
.client-rule-switch-track,
|
||||
.client-rule-switch-thumb,
|
||||
.client-local-rule-enabled circle,
|
||||
.client-local-rule-enabled path,
|
||||
.client-rule-type-trigger,
|
||||
.client-rule-type-trigger svg,
|
||||
.client-rule-type-list,
|
||||
|
||||
Reference in New Issue
Block a user