Polish routing rules UI and normalize pasted values
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
import { flushSync } from 'react-dom';
|
||||
import {
|
||||
canAppendRouteRule,
|
||||
normalizeRouteRules,
|
||||
ROUTE_RULES_CONTRACT_VERSION,
|
||||
} from '../../../shared/routingRules.js';
|
||||
import type { RouteRule } from '../../../shared/contracts/state.js';
|
||||
@@ -112,6 +113,14 @@ const sameRule = (left?: RouteRule, right?: RouteRule) => (
|
||||
Boolean(left && right && localRuleKey(left) === localRuleKey(right))
|
||||
);
|
||||
|
||||
const normalizeDraftRuleValue = (rule: DraftRule, value: string) => {
|
||||
try {
|
||||
return normalizeRouteRules([{ ...rule, value }], { strict: true })[0]?.value || value;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
function localRuleStatus(
|
||||
rule: DraftRule,
|
||||
index: number,
|
||||
@@ -382,27 +391,28 @@ export function useRoutingFeature({
|
||||
function positionPointerRule(pointerY: number) {
|
||||
const session = dragRef.current;
|
||||
if (!session?.lifted) return;
|
||||
const list = panelRef.current?.querySelector<HTMLElement>('.client-local-rules-list');
|
||||
if (!list) return;
|
||||
const rows = ruleRows();
|
||||
let row = rows.get(session.key);
|
||||
if (!row) return;
|
||||
let rect = row.getBoundingClientRect();
|
||||
const desiredCenter = pointerY - session.pointerOffsetY;
|
||||
const baseCenter = rect.top + rect.height / 2 - session.translateY;
|
||||
session.translateY = desiredCenter - baseCenter;
|
||||
const listTop = list.getBoundingClientRect().top;
|
||||
const slotCenter = (element: HTMLElement) => listTop + element.offsetTop + element.offsetHeight / 2;
|
||||
session.translateY = desiredCenter - slotCenter(row);
|
||||
row.style.setProperty('--client-rule-drag-y', `${session.translateY}px`);
|
||||
|
||||
const currentIndex = rulesRef.current.findIndex((rule) => rule._key === session.key);
|
||||
const centers = rulesRef.current.map((rule) => {
|
||||
const ruleRect = rows.get(rule._key)?.getBoundingClientRect();
|
||||
return ruleRect ? ruleRect.top + ruleRect.height / 2 : desiredCenter;
|
||||
const ruleRow = rows.get(rule._key);
|
||||
return ruleRow ? slotCenter(ruleRow) : desiredCenter;
|
||||
});
|
||||
const target = crossedRuleIndex(currentIndex, desiredCenter, centers);
|
||||
if (target === currentIndex) return;
|
||||
moveRuleTo(session.key, target);
|
||||
row = ruleRows().get(session.key);
|
||||
if (!row) return;
|
||||
rect = row.getBoundingClientRect();
|
||||
session.translateY = desiredCenter - (rect.top + rect.height / 2 - session.translateY);
|
||||
session.translateY = desiredCenter - slotCenter(row);
|
||||
row.style.setProperty('--client-rule-drag-y', `${session.translateY}px`);
|
||||
setReorderAnnouncement(`Правило перемещено на позицию ${target + 1}`);
|
||||
}
|
||||
@@ -896,6 +906,14 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
||||
Правила сохранены и начнут работать после запуска Harbor.
|
||||
</p>
|
||||
)}
|
||||
<details className="client-local-rules-help">
|
||||
<summary>Как работают правила</summary>
|
||||
<div>
|
||||
<p>Правила проверяются сверху вниз. Первое совпадение выбирает маршрут: щит означает VPN, стрелка — прямое подключение.</p>
|
||||
<p>Правила применяются только к трафику внутри маршрутизации Harbor. Gateway в режиме «Напрямую» и Connect при активном Harbor Gateway обходят локальный список.</p>
|
||||
<p>Можно вставить полный URL: Harbor сразу оставит только домен. Путь и параметры HTTPS для маршрутизации недоступны.</p>
|
||||
</div>
|
||||
</details>
|
||||
</header>
|
||||
|
||||
<form id="client-local-rules-form" className="client-local-rules-form" onSubmit={feature.save}>
|
||||
@@ -928,6 +946,7 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
||||
className={`client-local-rule client-deletable-row is-${status}${rule.enabled ? '' : ' is-disabled'}${rule.removing ? ' is-removing' : ''}${lifted ? ' is-dragging' : ''}`}
|
||||
key={rule._key}
|
||||
data-rule-key={rule._key}
|
||||
data-route={rule.outbound}
|
||||
role="listitem"
|
||||
style={{ viewTransitionName: rule.removing ? 'none' : rule._key }}
|
||||
inert={rule.removing ? true : undefined}
|
||||
@@ -954,20 +973,23 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
||||
<circle cx="6" cy="22" r="1.6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="client-local-rule-enabled"
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={rule.enabled}
|
||||
aria-label={`${rule.enabled ? 'Выключить' : 'Включить'} правило ${index + 1}`}
|
||||
disabled={editorDisabled}
|
||||
onClick={() => feature.change(index, 'enabled', !rule.enabled)}
|
||||
>
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true">
|
||||
<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>
|
||||
<span className="client-local-rule-enabled-wrap client-tooltip-anchor">
|
||||
<button
|
||||
className="client-local-rule-enabled"
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={rule.enabled}
|
||||
aria-label={`${rule.enabled ? 'Отключить' : 'Включить'} правило ${index + 1}`}
|
||||
disabled={editorDisabled}
|
||||
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" />
|
||||
</svg>
|
||||
</button>
|
||||
<Tooltip>{rule.enabled ? 'Отключить правило' : 'Включить правило'}</Tooltip>
|
||||
</span>
|
||||
<RuleTypePicker
|
||||
value={rule.type}
|
||||
ruleKey={rule._key}
|
||||
@@ -986,6 +1008,11 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
||||
placeholder={ROUTE_RULE_PLACEHOLDERS[rule.type]}
|
||||
value={rule.value}
|
||||
onChange={(event) => feature.change(index, 'value', event.target.value)}
|
||||
onPaste={(event) => {
|
||||
event.preventDefault();
|
||||
feature.change(index, 'value', normalizeDraftRuleValue(rule, event.clipboardData.getData('text')));
|
||||
}}
|
||||
onBlur={() => feature.change(index, 'value', normalizeDraftRuleValue(rule, rule.value))}
|
||||
/>
|
||||
<RuleOutboundPicker
|
||||
value={rule.outbound}
|
||||
@@ -1008,7 +1035,10 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
||||
disabled={editorDisabled}
|
||||
onClick={() => feature.remove(rule._key)}
|
||||
>
|
||||
×
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path className="client-row-delete-lid" d="M8 7V5h8v2m-11 0h14" />
|
||||
<path d="M7 7l1 13h8l1-13M10 10v7m4-7v7" />
|
||||
</svg>
|
||||
</button>
|
||||
<span
|
||||
className="client-delete-strike"
|
||||
@@ -1028,12 +1058,6 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p className="client-local-rules-note">
|
||||
Правила применяются только к трафику, который вошёл в VPN-маршрутизацию Harbor. Устройство Gateway в режиме «Напрямую» и Connect при активном Harbor Gateway обходят локальный список; «Напрямую» внутри правила — результат уже найденного совпадения.
|
||||
</p>
|
||||
<p className="client-local-rules-note">
|
||||
Можно вставить полный URL: Harbor сохранит только домен. Путь и параметры HTTPS недоступны для маршрутизации.
|
||||
</p>
|
||||
{statusSlot}
|
||||
<div className="client-local-rules-actions">
|
||||
<button type="button" onClick={feature.requestClose}>Отмена</button>
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
|
||||
.client-local-rule {
|
||||
--client-rule-drag-y: 0px;
|
||||
--client-rule-route-color: var(--client-accent);
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
min-width: 0;
|
||||
@@ -110,8 +111,28 @@
|
||||
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%);
|
||||
animation: client-row-enter 560ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
transition: background-color 180ms ease, box-shadow 180ms ease, opacity 260ms ease, filter 360ms ease;
|
||||
transition: background 320ms ease, box-shadow 180ms ease, opacity 260ms ease, filter 360ms ease;
|
||||
}
|
||||
|
||||
.client-local-rule[data-route='direct'] {
|
||||
--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 {
|
||||
@@ -140,23 +161,46 @@
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
border: 1px solid currentColor;
|
||||
background: transparent;
|
||||
opacity: 0.58;
|
||||
transition: color 280ms ease, opacity 280ms ease, filter 360ms ease, transform 280ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transition: color 280ms ease, border 280ms ease, border-radius 280ms ease, background 280ms ease, opacity 280ms ease, filter 360ms ease, transform 280ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-local-rule.is-active .client-rule-status-dot {
|
||||
color: var(--client-accent);
|
||||
border: 0;
|
||||
background: currentColor;
|
||||
opacity: 1;
|
||||
filter: drop-shadow(0 0 5px color-mix(in oklch, var(--client-accent) 52%, transparent));
|
||||
transform: scale(1.14);
|
||||
}
|
||||
|
||||
.client-local-rule.is-pending .client-rule-status-dot,
|
||||
.client-local-rule.is-unsaved .client-rule-status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border: 1.5px solid currentColor;
|
||||
border-radius: 1px;
|
||||
color: var(--client-warning, oklch(0.72 0.12 72));
|
||||
opacity: 1;
|
||||
filter: drop-shadow(0 0 4px color-mix(in oklch, var(--client-warning, oklch(0.72 0.12 72)) 44%, transparent));
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.client-local-rule.is-pending .client-rule-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border: 1.5px dashed currentColor;
|
||||
color: var(--client-warning, oklch(0.72 0.12 72));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.client-local-rule.is-disabled .client-rule-status-dot {
|
||||
width: 8px;
|
||||
height: 2px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--client-muted);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.client-local-rule.is-disabled > :not(.client-rule-handle):not(.client-row-delete):not(.client-delete-strike) {
|
||||
@@ -164,11 +208,16 @@
|
||||
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: color-mix(in oklch, var(--client-accent) 5%, transparent);
|
||||
box-shadow: 0 10px 28px color-mix(in oklch, var(--client-accent) 12%, transparent);
|
||||
background: linear-gradient(90deg, color-mix(in oklch, var(--client-rule-route-color) 13%, transparent), transparent 72%);
|
||||
box-shadow: 0 10px 28px color-mix(in oklch, var(--client-rule-route-color) 16%, transparent);
|
||||
transform: translateY(var(--client-rule-drag-y));
|
||||
}
|
||||
|
||||
@@ -177,6 +226,13 @@
|
||||
animation: client-row-leave 820ms cubic-bezier(0.7, 0, 0.84, 0) both;
|
||||
}
|
||||
|
||||
.client-local-rule-enabled-wrap {
|
||||
width: 24px;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.client-local-rule-enabled {
|
||||
width: 24px;
|
||||
height: 32px;
|
||||
@@ -228,43 +284,42 @@
|
||||
}
|
||||
|
||||
.client-local-rule-enabled svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
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-local-rule-enabled circle {
|
||||
stroke-width: 1.4;
|
||||
transition: fill 320ms ease, stroke 260ms ease;
|
||||
.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-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-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-local-rule-enabled[aria-checked='true'] {
|
||||
color: var(--client-accent);
|
||||
color: var(--client-rule-route-color);
|
||||
}
|
||||
|
||||
.client-local-rule-enabled[aria-checked='true'] svg {
|
||||
filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-accent) 48%, transparent));
|
||||
transform: scale(1.08);
|
||||
filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-rule-route-color) 42%, transparent));
|
||||
}
|
||||
|
||||
.client-local-rule-enabled[aria-checked='true'] circle {
|
||||
fill: color-mix(in oklch, var(--client-accent) 12%, transparent);
|
||||
.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'] .client-rule-check {
|
||||
stroke-dashoffset: 0;
|
||||
.client-local-rule-enabled[aria-checked='true'] .client-rule-switch-thumb {
|
||||
transform: translateX(8px);
|
||||
}
|
||||
|
||||
.client-rule-type {
|
||||
@@ -422,15 +477,11 @@
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: var(--client-muted);
|
||||
color: var(--client-rule-route-color);
|
||||
cursor: pointer;
|
||||
transition: color 220ms ease, filter 320ms ease, background 220ms ease;
|
||||
}
|
||||
|
||||
.client-rule-outbound button.is-direct {
|
||||
color: var(--client-accent);
|
||||
}
|
||||
|
||||
.client-rule-outbound svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -448,15 +499,13 @@
|
||||
|
||||
.client-rule-outbound button:focus-visible {
|
||||
outline: 0;
|
||||
background: color-mix(in oklch, var(--client-accent) 10%, transparent);
|
||||
color: var(--client-accent);
|
||||
filter: drop-shadow(0 0 5px color-mix(in oklch, var(--client-accent) 36%, transparent));
|
||||
background: color-mix(in oklch, var(--client-rule-route-color) 10%, transparent);
|
||||
filter: drop-shadow(0 0 5px color-mix(in oklch, var(--client-rule-route-color) 36%, transparent));
|
||||
}
|
||||
|
||||
.client-rule-outbound button:hover:not(:disabled) {
|
||||
background: color-mix(in oklch, var(--client-accent) 10%, transparent);
|
||||
color: var(--client-accent);
|
||||
filter: drop-shadow(0 0 5px color-mix(in oklch, var(--client-accent) 36%, transparent));
|
||||
background: color-mix(in oklch, var(--client-rule-route-color) 10%, transparent);
|
||||
filter: drop-shadow(0 0 5px color-mix(in oklch, var(--client-rule-route-color) 36%, transparent));
|
||||
}
|
||||
|
||||
.client-rule-outbound button.is-direct:hover:not(:disabled) svg,
|
||||
@@ -490,8 +539,42 @@
|
||||
text-transform: var(--type-control-transform);
|
||||
}
|
||||
|
||||
.client-local-rules-note {
|
||||
.client-local-rules-help {
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.client-local-rules-help summary {
|
||||
width: fit-content;
|
||||
color: var(--client-muted);
|
||||
font: var(--type-label);
|
||||
letter-spacing: var(--type-label-tracking);
|
||||
text-transform: var(--type-label-transform);
|
||||
cursor: pointer;
|
||||
transition: color 220ms ease, text-shadow 300ms ease;
|
||||
}
|
||||
|
||||
.client-local-rules-help summary:hover,
|
||||
.client-local-rules-help summary:focus-visible {
|
||||
outline: 0;
|
||||
color: var(--client-accent);
|
||||
text-shadow: 0 0 9px color-mix(in oklch, var(--client-accent) 34%, transparent);
|
||||
}
|
||||
|
||||
.client-local-rules-help > div {
|
||||
max-width: 48ch;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in oklch, var(--client-accent) 5%, transparent);
|
||||
animation: client-local-rules-notice 280ms ease both;
|
||||
}
|
||||
|
||||
.client-local-rules-help > div p {
|
||||
max-width: 48ch;
|
||||
margin: 0;
|
||||
color: var(--client-muted);
|
||||
font: var(--type-label);
|
||||
letter-spacing: var(--type-label-tracking);
|
||||
|
||||
@@ -710,7 +710,7 @@
|
||||
}
|
||||
|
||||
.client-rule-handle,
|
||||
.client-local-rule-enabled,
|
||||
.client-local-rule-enabled-wrap,
|
||||
.client-row-delete {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
@@ -752,7 +752,7 @@
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.client-local-rule-enabled {
|
||||
.client-local-rule-enabled-wrap {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
@@ -287,12 +287,11 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.client-deletable-row.is-removing > :not(.client-delete-strike):not(.client-local-rule-enabled) {
|
||||
.client-deletable-row.is-removing > :not(.client-delete-strike) {
|
||||
animation: client-delete-content-dim 820ms ease-out both;
|
||||
}
|
||||
|
||||
.client-deletable-row.is-removing .client-local-rule-enabled circle,
|
||||
.client-deletable-row.is-removing .client-rule-check {
|
||||
.client-deletable-row.is-removing .client-rule-switch-thumb {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -311,9 +310,33 @@
|
||||
.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));
|
||||
}
|
||||
|
||||
.client-row-delete:hover:not(:has(svg)) {
|
||||
transform: rotate(8deg) scale(1.1);
|
||||
}
|
||||
|
||||
.client-row-delete svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
overflow: visible;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.6;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.client-row-delete-lid {
|
||||
transform-origin: center 7px;
|
||||
transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-row-delete:hover:not(:disabled) .client-row-delete-lid,
|
||||
.client-row-delete:focus-visible .client-row-delete-lid {
|
||||
transform: translateY(-2px) rotate(-8deg);
|
||||
}
|
||||
|
||||
.client-rule-handle:focus-visible,
|
||||
.client-local-rule-enabled:focus-visible,
|
||||
.client-rule-type-trigger:focus-visible,
|
||||
|
||||
@@ -122,10 +122,11 @@
|
||||
}
|
||||
|
||||
.client-local-rule,
|
||||
.client-local-rule::after,
|
||||
.client-rule-handle,
|
||||
.client-local-rule-enabled svg,
|
||||
.client-local-rule-enabled circle,
|
||||
.client-local-rule-enabled path,
|
||||
.client-rule-switch-track,
|
||||
.client-rule-switch-thumb,
|
||||
.client-rule-type-trigger,
|
||||
.client-rule-type-trigger svg,
|
||||
.client-rule-type-list,
|
||||
@@ -135,9 +136,13 @@
|
||||
.client-rule-outbound svg,
|
||||
.client-rule-status-dot,
|
||||
.client-row-delete,
|
||||
.client-row-delete svg,
|
||||
.client-row-delete-lid,
|
||||
.client-row-add,
|
||||
.client-row-add-slot > span,
|
||||
.client-local-rules-save,
|
||||
.client-local-rules-help summary,
|
||||
.client-local-rules-help > div,
|
||||
.client-local-rules-runtime,
|
||||
.client-local-rules-actions button {
|
||||
transition: none;
|
||||
|
||||
Reference in New Issue
Block a user