Simplify rail motion and add routing help toggle
Build and Deploy Gateway / build-and-push (push) Successful in 35s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-08-17 17:26:57 +03:00
parent 0a0a932057
commit dc1fd76c44
11 changed files with 91 additions and 221 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({ export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.26.4', macClient: '0.26.5',
gatewayClient: '0.27.4', gatewayClient: '0.27.5',
gatewayBackend: '0.27.0', gatewayBackend: '0.27.0',
}); });
@@ -249,8 +249,6 @@ export function DevicesToggle({
</g> </g>
<rect className="client-rail-device-phone" x="16.5" y="7" width="5" height="9" rx="1.3" /> <rect className="client-rail-device-phone" x="16.5" y="7" width="5" height="9" rx="1.3" />
<path className="client-rail-device-link" d="M19 16v3h-5.5" /> <path className="client-rail-device-link" d="M19 16v3h-5.5" />
<path className="client-rail-device-bridge" d="M10.5 19h3" />
<path className="client-rail-device-packet" pathLength="1" d="M7 19h12" />
</svg> </svg>
</RailAction>; </RailAction>;
} }
@@ -61,7 +61,6 @@ export function DiagnosticsToggle({
> >
<svg viewBox="0 0 24 24" aria-hidden="true"> <svg viewBox="0 0 24 24" aria-hidden="true">
<path className="client-rail-diagnostics-trace" d="M4.5 10h2l1.2-2.4 2.2 4.8 1.2-2.4h3.4" /> <path className="client-rail-diagnostics-trace" d="M4.5 10h2l1.2-2.4 2.2 4.8 1.2-2.4h3.4" />
<path className="client-rail-diagnostics-scan" pathLength="1" d="M4.5 10h2l1.2-2.4 2.2 4.8 1.2-2.4h3.4" />
<g className="client-rail-diagnostics-position"> <g className="client-rail-diagnostics-position">
<g className="client-rail-diagnostics-glass"> <g className="client-rail-diagnostics-glass">
<circle cx="9.5" cy="9.5" r="6.5" /> <circle cx="9.5" cy="9.5" r="6.5" />
+19 -8
View File
@@ -863,6 +863,7 @@ export function RoutingPendingStatus({
} }
export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature; statusSlot?: ReactNode }) { export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature; statusSlot?: ReactNode }) {
const [helpOpen, setHelpOpen] = useState(false);
const draftRules = feature.rules.filter((rule) => !rule.removing); const draftRules = feature.rules.filter((rule) => !rule.removing);
const editorDisabled = feature.blocked || !feature.editable; const editorDisabled = feature.blocked || !feature.editable;
const canAdd = canAppendRouteRule(draftRules) && !editorDisabled; const canAdd = canAppendRouteRule(draftRules) && !editorDisabled;
@@ -891,7 +892,6 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
Сохранить Сохранить
</button> </button>
<h2 id="local-rules-title">Правила маршрутизации</h2> <h2 id="local-rules-title">Правила маршрутизации</h2>
<p>Проверяются сверху вниз. Первое совпадение выбирает маршрут.</p>
{!feature.editable && ( {!feature.editable && (
<p className="client-local-rules-runtime" role="status"> <p className="client-local-rules-runtime" role="status">
Обновите Harbor, чтобы редактировать правила. Обновите Harbor, чтобы редактировать правила.
@@ -912,14 +912,25 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
Правила сохранены и начнут работать после запуска Harbor. Правила сохранены и начнут работать после запуска Harbor.
</p> </p>
)} )}
<details className="client-local-rules-help"> <div className={`client-local-rules-help${helpOpen ? ' is-open' : ''}`}>
<summary>Как работают правила</summary> <button
<div> type="button"
<p>Правила проверяются сверху вниз. Первое совпадение выбирает маршрут: щит означает VPN, стрелка прямое подключение.</p> aria-expanded={helpOpen}
<p>Правила применяются только к трафику внутри маршрутизации Harbor. Gateway в режиме «Напрямую» и Connect при активном Harbor Gateway обходят локальный список.</p> aria-controls="client-local-rules-help-content"
<p>Можно вставить полный URL: Harbor сразу оставит только домен. Путь и параметры HTTPS для маршрутизации недоступны.</p> onClick={() => setHelpOpen((current) => !current)}
>
Как работают правила
</button>
<div className="client-local-rules-help-reveal" aria-hidden={!helpOpen}>
<div id="client-local-rules-help-content" className="client-local-rules-help-content">
<div className="client-local-rules-help-copy">
<p>Правила проверяются сверху вниз. Первое совпадение выбирает маршрут: щит означает VPN, стрелка прямое подключение.</p>
<p>Правила применяются только к трафику внутри маршрутизации Harbor. Gateway в режиме «Напрямую» и Connect при активном Harbor Gateway обходят локальный список.</p>
<p>Можно вставить полный URL: Harbor сразу оставит только домен. Путь и параметры HTTPS для маршрутизации недоступны.</p>
</div>
</div>
</div> </div>
</details> </div>
</header> </header>
<form id="client-local-rules-form" className="client-local-rules-form" onSubmit={feature.save}> <form id="client-local-rules-form" className="client-local-rules-form" onSubmit={feature.save}>
@@ -322,7 +322,6 @@ export function SubscriptionToggle({
<g className="client-rail-subscription-front"> <g className="client-rail-subscription-front">
<rect className="client-rail-subscription-card" x="4" y="6" width="14" height="14" rx="2" /> <rect className="client-rail-subscription-card" x="4" y="6" width="14" height="14" rx="2" />
<path className="client-rail-subscription-lines" d="M7.5 10.5h6.5M7.5 14.5H12" /> <path className="client-rail-subscription-lines" d="M7.5 10.5h6.5M7.5 14.5H12" />
<path className="client-rail-subscription-scan" pathLength="1" d="M7.5 10.5H14" />
</g> </g>
</svg> </svg>
</RailAction>; </RailAction>;
+31 -6
View File
@@ -537,8 +537,11 @@
margin-top: 4px; margin-top: 4px;
} }
.client-local-rules-help summary { .client-local-rules-help > button {
width: fit-content; width: fit-content;
padding: 0;
border: 0;
background: transparent;
color: var(--client-muted); color: var(--client-muted);
font: var(--type-label); font: var(--type-label);
letter-spacing: var(--type-label-tracking); letter-spacing: var(--type-label-tracking);
@@ -547,14 +550,31 @@
transition: color 220ms ease, text-shadow 300ms ease; transition: color 220ms ease, text-shadow 300ms ease;
} }
.client-local-rules-help summary:hover, .client-local-rules-help > button:hover,
.client-local-rules-help summary:focus-visible { .client-local-rules-help > button:focus-visible {
outline: 0; outline: 0;
color: var(--client-accent); color: var(--client-accent);
text-shadow: 0 0 9px color-mix(in oklch, var(--client-accent) 34%, transparent); text-shadow: 0 0 9px color-mix(in oklch, var(--client-accent) 34%, transparent);
} }
.client-local-rules-help > div { .client-local-rules-help-reveal {
display: grid;
grid-template-rows: 0fr;
opacity: 0;
transition: grid-template-rows 420ms cubic-bezier(0.16, 1, 0.3, 1), opacity 220ms ease;
}
.client-local-rules-help.is-open .client-local-rules-help-reveal {
grid-template-rows: 1fr;
opacity: 1;
}
.client-local-rules-help-content {
min-height: 0;
overflow: hidden;
}
.client-local-rules-help-copy {
max-width: 48ch; max-width: 48ch;
display: grid; display: grid;
gap: 8px; gap: 8px;
@@ -562,10 +582,15 @@
padding: 10px 12px; padding: 10px 12px;
border-radius: 10px; border-radius: 10px;
background: color-mix(in oklch, var(--client-accent) 5%, transparent); background: color-mix(in oklch, var(--client-accent) 5%, transparent);
animation: client-local-rules-notice 280ms ease both; transform: translateY(-6px);
transition: transform 420ms cubic-bezier(0.16, 1, 0.3, 1);
} }
.client-local-rules-help > div p { .client-local-rules-help.is-open .client-local-rules-help-copy {
transform: translateY(0);
}
.client-local-rules-help-copy p {
max-width: 48ch; max-width: 48ch;
margin: 0; margin: 0;
color: var(--client-muted); color: var(--client-muted);
-148
View File
@@ -8,7 +8,6 @@
} }
.client-rail-action { .client-rail-action {
--client-rail-state-duration: 340ms;
position: relative; position: relative;
width: 42px; width: 42px;
height: 48px; height: 48px;
@@ -19,7 +18,6 @@
background: transparent; background: transparent;
color: var(--client-muted); color: var(--client-muted);
cursor: pointer; cursor: pointer;
transition: color 240ms ease, transform 440ms cubic-bezier(0.16, 1, 0.3, 1);
} }
.client-rail-action svg { .client-rail-action svg {
@@ -31,12 +29,6 @@
stroke-linecap: round; stroke-linecap: round;
stroke-linejoin: round; stroke-linejoin: round;
overflow: visible; overflow: visible;
transition: color 240ms ease, filter 360ms ease, transform 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-rail-action:active:not(:disabled) svg {
transform: scale(0.94);
transition: color 120ms ease, filter 120ms ease, transform 120ms cubic-bezier(0.16, 1, 0.3, 1);
} }
.client-rail-action > span { .client-rail-action > span {
@@ -58,7 +50,6 @@
.client-rail-action:focus-visible { .client-rail-action:focus-visible {
outline: 0; outline: 0;
color: var(--client-accent); color: var(--client-accent);
transform: translateX(-3px);
} }
.client-rail-action:hover > span, .client-rail-action:hover > span,
@@ -74,157 +65,19 @@
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));
} }
.client-rail-subscription-back,
.client-rail-subscription-front,
.client-rail-book-page-position,
.client-rail-book-spine,
.client-rail-device-link,
.client-rail-device-bridge,
.client-rail-diagnostics-position,
.client-rail-rule-knob-position {
transform-box: fill-box;
transform-origin: center;
transition: transform var(--client-rail-state-duration) cubic-bezier(0.16, 1, 0.3, 1), opacity 260ms ease, stroke-dasharray var(--client-rail-state-duration) cubic-bezier(0.16, 1, 0.3, 1), stroke-dashoffset var(--client-rail-state-duration) cubic-bezier(0.16, 1, 0.3, 1);
}
.client-rail-book-page,
.client-rail-diagnostics-glass,
.client-rail-rule-knob {
transform-box: fill-box;
transform-origin: center;
}
.client-rail-book-page.is-right {
transform-origin: left center;
}
.client-rail-action .client-rail-subscription-card, .client-rail-action .client-rail-subscription-card,
.client-rail-action .client-rail-rule-knob { .client-rail-action .client-rail-rule-knob {
fill: var(--client-bg); fill: var(--client-bg);
} }
.client-rail-subscription-lines {
opacity: 1;
}
.client-subscription-toggle.is-open .client-rail-subscription-back {
transform: translate(1.5px, -1px);
}
.client-subscription-toggle.is-open .client-rail-subscription-front {
transform: translate(-2px, 1px);
}
.client-instructions-toggle.is-open .client-rail-book-page-position.is-left {
transform: translateX(-1.25px) rotate(-2.5deg);
}
.client-instructions-toggle.is-open .client-rail-book-page-position.is-right {
transform: translateX(1.25px) rotate(2.5deg);
}
.client-instructions-toggle.is-open .client-rail-book-spine {
transform: scaleY(1.06);
}
.client-rail-device-link { .client-rail-device-link {
opacity: 0.68; opacity: 0.68;
} }
.client-rail-device-bridge {
opacity: 0;
transform: scaleX(0);
}
.client-rail-subscription-scan,
.client-rail-device-packet,
.client-rail-diagnostics-scan {
stroke-width: 2.4;
opacity: 0;
stroke-dasharray: 0.18 0.82;
stroke-dashoffset: 1;
}
.client-rail-device-packet {
stroke-dasharray: 0.36 0.64;
}
.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-rail-diagnostics-trace { .client-rail-diagnostics-trace {
opacity: 0.72; opacity: 0.72;
} }
.client-diagnostics-toggle.is-open .client-rail-diagnostics-position {
transform: translate(2px, -1px) rotate(-7deg);
}
.client-local-rules-toggle.is-open .client-rail-rule-knob-position.is-top {
transform: translateX(4px);
}
.client-local-rules-toggle.is-open .client-rail-rule-knob-position.is-bottom {
transform: translateX(-4px);
}
.client-subscription-toggle.is-open .client-rail-subscription-scan,
.client-devices-toggle.is-open .client-rail-device-packet,
.client-diagnostics-toggle.is-open .client-rail-diagnostics-scan {
animation: client-rail-ambient-trace 10s 1.1s linear infinite;
}
.client-instructions-toggle.is-open .client-rail-book-page.is-right {
animation: client-rail-book-turn 10s 1.1s ease-in-out infinite;
}
.client-diagnostics-toggle.is-open .client-rail-diagnostics-glass {
animation: client-rail-glass-scan 10s 1.1s ease-in-out infinite;
}
.client-local-rules-toggle.is-open .client-rail-rule-knob.is-top {
animation: client-rail-rule-tune-top 10s 1.1s ease-in-out infinite;
}
.client-local-rules-toggle.is-open .client-rail-rule-knob.is-bottom {
animation: client-rail-rule-tune-bottom 10s 1.28s ease-in-out infinite;
}
@keyframes client-rail-ambient-trace {
0% { opacity: 0; stroke-dashoffset: 1; }
2% { opacity: 1; }
8% { opacity: 1; stroke-dashoffset: 0; }
10%, 100% { opacity: 0; stroke-dashoffset: 0; }
}
@keyframes client-rail-book-turn {
0%, 2%, 10%, 100% { transform: translateX(0) scaleX(1); }
5% { transform: translateX(-1px) scaleX(0.18); }
7% { transform: translateX(-0.5px) scaleX(0.55); }
}
@keyframes client-rail-glass-scan {
0%, 2%, 10%, 100% { transform: translateX(0) rotate(0); }
4% { transform: translateX(-3px) rotate(6deg); }
7% { transform: translateX(1.5px) rotate(-3deg); }
}
@keyframes client-rail-rule-tune-top {
0%, 2%, 10%, 100% { transform: translateX(0); }
6% { transform: translateX(-2px); }
}
@keyframes client-rail-rule-tune-bottom {
0%, 2%, 10%, 100% { transform: translateX(0); }
6% { transform: translateX(2px); }
}
.client-local-rules-toggle:disabled { .client-local-rules-toggle:disabled {
cursor: default; cursor: default;
color: var(--client-muted); color: var(--client-muted);
@@ -244,7 +97,6 @@
} }
.client-rail-action.is-open { .client-rail-action.is-open {
--client-rail-state-duration: 480ms;
color: var(--client-accent); color: var(--client-accent);
} }
+4 -6
View File
@@ -117,10 +117,6 @@
stroke-dashoffset: 0; stroke-dashoffset: 0;
} }
.client-rail-action:active:not(:disabled) svg {
transform: none;
}
.client-local-rule, .client-local-rule,
.client-rule-handle, .client-rule-handle,
.client-rule-handle svg, .client-rule-handle svg,
@@ -141,8 +137,10 @@
.client-row-add, .client-row-add,
.client-row-add-slot > span, .client-row-add-slot > span,
.client-local-rules-save, .client-local-rules-save,
.client-local-rules-help summary, .client-local-rules-help > button,
.client-local-rules-help > div, .client-local-rules-help-reveal,
.client-local-rules-help-content,
.client-local-rules-help-copy,
.client-local-rules-runtime, .client-local-rules-runtime,
.client-local-rules-actions button { .client-local-rules-actions button {
transition: none; transition: none;
+9 -26
View File
@@ -139,36 +139,19 @@ test('secondary menus share one right rail and switch equal drawers as a vertica
assert.match(disabledRulesLabel, /opacity:\s*0/); assert.match(disabledRulesLabel, /opacity:\s*0/);
assert.match(disabledRulesLabel, /filter:\s*blur\(5px\)/); assert.match(disabledRulesLabel, /filter:\s*blur\(5px\)/);
assert.match(styles, /\.client-local-rules-toggle:disabled:hover span\s*\{[\s\S]*opacity:\s*1/); assert.match(styles, /\.client-local-rules-toggle:disabled:hover span\s*\{[\s\S]*opacity:\s*1/);
assert.match(subscription, /client-rail-subscription-back[\s\S]*client-rail-subscription-front[\s\S]*client-rail-subscription-lines[\s\S]*client-rail-subscription-scan/); assert.match(subscription, /client-rail-subscription-back[\s\S]*client-rail-subscription-front[\s\S]*client-rail-subscription-lines/);
assert.match(instructions, /client-rail-book-page-position is-left[\s\S]*client-rail-book-page is-left[\s\S]*client-rail-book-page-position is-right[\s\S]*client-rail-book-page is-right[\s\S]*client-rail-book-spine/); assert.match(instructions, /client-rail-book-page-position is-left[\s\S]*client-rail-book-page is-left[\s\S]*client-rail-book-page-position is-right[\s\S]*client-rail-book-page is-right[\s\S]*client-rail-book-spine/);
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-rail-diagnostics-trace[\s\S]*client-rail-diagnostics-scan[\s\S]*client-rail-diagnostics-position[\s\S]*client-rail-diagnostics-glass/); assert.match(diagnosticsFeature, /client-rail-diagnostics-trace[\s\S]*client-rail-diagnostics-position[\s\S]*client-rail-diagnostics-glass/);
assert.match(devices, /client-rail-device-monitor[\s\S]*M5 19h5\.5M7 15v4[\s\S]*client-rail-device-phone[\s\S]*client-rail-device-link[\s\S]*M19 16v3h-5\.5[\s\S]*client-rail-device-bridge[\s\S]*M10\.5 19h3[\s\S]*client-rail-device-packet[\s\S]*M7 19h12/); assert.match(devices, /client-rail-device-monitor[\s\S]*M5 19h5\.5M7 15v4[\s\S]*client-rail-device-phone[\s\S]*client-rail-device-link[\s\S]*M19 16v3h-5\.5/);
assert.match(routing, /client-rail-rule-track[\s\S]*client-rail-rule-knob-position is-top[\s\S]*client-rail-rule-knob is-top[\s\S]*client-rail-rule-knob-position is-bottom[\s\S]*client-rail-rule-knob is-bottom/); assert.match(routing, /client-rail-rule-track[\s\S]*client-rail-rule-knob-position is-top[\s\S]*client-rail-rule-knob is-top[\s\S]*client-rail-rule-knob-position is-bottom[\s\S]*client-rail-rule-knob is-bottom/);
assert.match(styles, /\.client-rail-action:active:not\(:disabled\) svg \{[\s\S]*scale\(0\.94\)[\s\S]*120ms/);
assert.match(styles, /\.client-rail-action svg \{[\s\S]*stroke-width:\s*1\.75/); assert.match(styles, /\.client-rail-action svg \{[\s\S]*stroke-width:\s*1\.75/);
assert.match(styles, /\.client-subscription-toggle\.is-open \.client-rail-subscription-back \{[\s\S]*translate\(1\.5px, -1px\)/);
assert.match(styles, /\.client-subscription-toggle\.is-open \.client-rail-subscription-front \{[\s\S]*translate\(-2px, 1px\)/);
assert.match(styles, /\.client-instructions-toggle\.is-open \.client-rail-book-page-position\.is-left \{[\s\S]*translateX\(-1\.25px\) rotate\(-2\.5deg\)/);
assert.match(styles, /\.client-instructions-toggle\.is-open \.client-rail-book-page-position\.is-right \{[\s\S]*translateX\(1\.25px\) rotate\(2\.5deg\)/);
assert.match(styles, /@keyframes client-rail-book-turn[\s\S]*scaleX\(0\.18\)[\s\S]*scaleX\(0\.55\)/);
assert.doesNotMatch(styles, /client-rail-info|stroke-dasharray:\s*0\.9 0\.1/);
assert.match(styles, /\.client-devices-toggle\.is-open \.client-rail-device-link \{[\s\S]*opacity:\s*1/);
assert.match(styles, /\.client-devices-toggle\.is-open \.client-rail-device-bridge \{[\s\S]*scaleX\(1\)/);
assert.match(styles, /\.client-diagnostics-toggle\.is-open \.client-rail-diagnostics-position \{[\s\S]*translate\(2px, -1px\) rotate\(-7deg\)/);
assert.match(styles, /@keyframes client-rail-glass-scan[\s\S]*translateX\(-3px\) rotate\(6deg\)[\s\S]*translateX\(1\.5px\) rotate\(-3deg\)/);
assert.match(routing, /client-rail-rule-knob is-top" cx="15"[\s\S]*client-rail-rule-knob is-bottom" cx="9"/); assert.match(routing, /client-rail-rule-knob is-top" cx="15"[\s\S]*client-rail-rule-knob is-bottom" cx="9"/);
assert.match(styles, /\.client-local-rules-toggle\.is-open \.client-rail-rule-knob-position\.is-top \{[\s\S]*translateX\(4px\)/); assert.match(rule('.client-rail-action.is-open'), /color:\s*var\(--client-accent\)/);
assert.match(styles, /\.client-local-rules-toggle\.is-open \.client-rail-rule-knob-position\.is-bottom \{[\s\S]*translateX\(-4px\)/); assert.doesNotMatch(`${subscription}\n${devices}\n${diagnosticsFeature}`, /client-rail-(?:subscription-scan|device-bridge|device-packet|diagnostics-scan)/);
assert.match(styles, /@keyframes client-rail-rule-tune-top[\s\S]*translateX\(-2px\)/); assert.doesNotMatch(styles, /client-rail-(?:ambient-trace|book-turn|glass-scan|rule-tune)|--client-rail-state-duration/);
assert.match(styles, /@keyframes client-rail-rule-tune-bottom[\s\S]*translateX\(2px\)/); assert.doesNotMatch(styles, /\.client-(?:subscription|instructions|devices|diagnostics|local-rules)-toggle\.is-open [^{]+\{[^}]*(?:transform|animation)/);
assert.match(styles, /\.client-subscription-toggle\.is-open \.client-rail-subscription-scan,[\s\S]*\.client-diagnostics-toggle\.is-open \.client-rail-diagnostics-scan \{[\s\S]*client-rail-ambient-trace 10s 1\.1s/); assert.doesNotMatch(styles, /\.client-rail-action:active|\.client-rail-action:hover,[\s\S]*?transform:\s*translateX\(-3px\)/);
assert.match(styles, /@keyframes client-rail-ambient-trace[\s\S]*stroke-dashoffset: 1[\s\S]*stroke-dashoffset: 0/);
assert.match(styles, /--client-rail-state-duration:\s*340ms/);
assert.match(styles, /\.client-rail-action\.is-open \{[\s\S]*--client-rail-state-duration:\s*480ms/);
assert.match(styles, /transition: transform var\(--client-rail-state-duration\) cubic-bezier\(0\.16, 1, 0\.3, 1\)/);
assert.doesNotMatch(styles, /client-rail-info|client-rail-device-primary|client-rail-diagnostics-base|client-rail-rule-scan|client-rail-rule-pulse/);
assert.doesNotMatch(styles, /client-secondary-icon-motion|\.client-local-rules-toggle\.is-open svg\s*\{[^}]*rotate/);
assert.match(rule('.client-drawer'), /inset:\s*0 0 0 auto/); assert.match(rule('.client-drawer'), /inset:\s*0 0 0 auto/);
assert.match(rule('.client-drawer'), /width:\s*min\(580px, 100vw\)/); assert.match(rule('.client-drawer'), /width:\s*min\(580px, 100vw\)/);
assert.match(rule('.client-drawer'), /transform:\s*translateX\(104%\)/); assert.match(rule('.client-drawer'), /transform:\s*translateX\(104%\)/);
@@ -271,7 +254,7 @@ test('responsive motion has a reduced-motion fallback', () => {
assert.match(reducedMotion, /\.client-drawer/); 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/);
assert.match(reducedMotion, /\.client-rail-action:active:not\(:disabled\) svg[\s\S]*transform:\s*none/); assert.doesNotMatch(styles, /\.client-rail-action:active:not\(:disabled\) svg/);
}); });
test('tooltips stay opaque, above adjacent content, and do not stick after pointer clicks', () => { test('tooltips stay opaque, above adjacent content, and do not stick after pointer clicks', () => {
+8 -3
View File
@@ -135,11 +135,16 @@ test('ordered rules use one accessible drag handle and a compact icon route cont
assert.match(styles, /\.client-local-rule \+ \.client-local-rule::before[\s\S]*linear-gradient\(90deg/); assert.match(styles, /\.client-local-rule \+ \.client-local-rule::before[\s\S]*linear-gradient\(90deg/);
}); });
test('rule values normalize on paste and help stays collapsed above the list', () => { test('rule values normalize on paste and help animates both directions above the list', () => {
assert.match(routing, /normalizeRouteRules\(\[\{ \.\.\.rule, value \}\], \{ strict: true \}\)/); assert.match(routing, /normalizeRouteRules\(\[\{ \.\.\.rule, value \}\], \{ strict: true \}\)/);
assert.match(routing, /onPaste=\{\(event\) => \{[\s\S]*event\.preventDefault\(\)[\s\S]*normalizeDraftRuleValue\(rule, event\.clipboardData\.getData\('text'\)\)/); assert.match(routing, /onPaste=\{\(event\) => \{[\s\S]*event\.preventDefault\(\)[\s\S]*normalizeDraftRuleValue\(rule, event\.clipboardData\.getData\('text'\)\)/);
assert.match(routing, /onBlur=\{\(\) => feature\.change\(index, 'value', normalizeDraftRuleValue\(rule, rule\.value\)\)\}/); assert.match(routing, /onBlur=\{\(\) => feature\.change\(index, 'value', normalizeDraftRuleValue\(rule, rule\.value\)\)\}/);
assert.match(routing, /<details className="client-local-rules-help">[\s\S]*<summary>[\s\S]*Как работают правила[\s\S]*<form id="client-local-rules-form"/); assert.equal((routing.match(/Проверяются сверху вниз\. Первое совпадение выбирает маршрут/g) || []).length, 0);
assert.equal((routing.match(/Правила проверяются сверху вниз\. Первое совпадение выбирает маршрут/g) || []).length, 1);
assert.match(routing, /const \[helpOpen, setHelpOpen\] = useState\(false\)[\s\S]*aria-expanded=\{helpOpen\}[\s\S]*aria-controls="client-local-rules-help-content"[\s\S]*setHelpOpen\(\(current\) => !current\)[\s\S]*Как работают правила[\s\S]*aria-hidden=\{!helpOpen\}[\s\S]*<form id="client-local-rules-form"/);
assert.doesNotMatch(routing, /client-local-rules-note/); assert.doesNotMatch(routing, /client-local-rules-note/);
assert.match(styles, /\.client-local-rules-help > div \{[\s\S]*background: color-mix/); assert.match(styles, /\.client-local-rules-help-reveal \{[\s\S]*grid-template-rows:\s*0fr[\s\S]*opacity:\s*0[\s\S]*420ms cubic-bezier\(0\.16, 1, 0\.3, 1\)/);
assert.match(styles, /\.client-local-rules-help\.is-open \.client-local-rules-help-reveal \{[\s\S]*grid-template-rows:\s*1fr[\s\S]*opacity:\s*1/);
assert.match(styles, /\.client-local-rules-help-content \{[\s\S]*overflow:\s*hidden/);
assert.match(styles, /\.client-local-rules-help-copy \{[\s\S]*background: color-mix[\s\S]*translateY\(-6px\)[\s\S]*transition:\s*transform 420ms/);
}); });
+18 -18
View File
@@ -37,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: 950, cascadeEdges: 949,
customProperties: 108, customProperties: 106,
declarations: 3516, declarations: 3474,
important: 0, important: 0,
keyframes: 53, keyframes: 48,
media: 13, media: 13,
rules: 1000, rules: 966,
variableReferences: 840, variableReferences: 837,
}, },
hashes: { hashes: {
cascadeEdges: '5d255c2f624bf3b45037c027f127d644f02dfc5a130313e766597da9bee4875f', cascadeEdges: 'a2289354130ffe83c907b977d21d773799c4435c8016e51d14f153184f834804',
customProperties: '3b97add4c3d685a532afff5046ddbe2b6eebcc0866c62781348559897f2930fb', customProperties: 'fd6f16069f9fe3526f09d4d574431ddae67150d8e7a8a40e04c9fd2d179ec7ac',
declarations: '4cf0d3ed382d4757bf7313ea9e941167ccbb28293198e89852fda855b288f492', declarations: '6581f90631086ad0ba2ef1b3d1ec86f608e872572af5486d4c7199f06802bcfd',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: '8982145dba05b33bf1c93b2d54cfbe76305b276ff3c657aa020144016ada5848', duplicateSelectors: '8982145dba05b33bf1c93b2d54cfbe76305b276ff3c657aa020144016ada5848',
keyframes: '78b90f6f08db3eb7c998d6be48799a1fff9179c416b6e4364c0efdeb3591e4d1', keyframes: 'a0d69c5b3e5f235d3a76ed04bdd64f67a73fafc9c59fe2635ffda603709c0ad9',
ruleDeclarationSequences: '8d146a746adac504897039e31408dc342e8e9f2940c2984d69b4b21b4d6a8e3f', ruleDeclarationSequences: 'dc793f9ea84540c87ae3650fa915efed643b941c393e8dd0fd75fcb01c543bb3',
selectors: 'fb7382b9ad1e8a7593553ce7f2544647882a1089c967e30623bb48344da0b490', selectors: 'a556fe1d9608d73332db5b3c14cd4cfde42f3346a542d3e2b2ce2af34f556c8c',
variableReferences: '1106c0a87671667957261a7f80cdab657d7a5f0078a9feece09c1efcad4f6455', variableReferences: '843a5f71b0fb74691ed623dbc7ebb7ddddc34e5736e1f419f17cf54e22d43073',
witnesses: '88e71ed6ab4be07b2c7b45f90677c2f70dbc5d5b7dc8a42c8715e3f8084096dc', witnesses: '10b015073632f8697ac16b5849f28029322d9164060896c366ba4d35dbb3cee5',
}, },
}; };
@@ -209,7 +209,7 @@ test('client typography uses the shared semantic scale outside the token owner',
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, 863); assert.equal(witnesses.length, 860);
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0); assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
const ledger = createStyleLedger(readStyleSource(root), { witnesses }); const ledger = createStyleLedger(readStyleSource(root), { witnesses });
assert.deepEqual(ledger.counts, acceptedLedger.counts); assert.deepEqual(ledger.counts, acceptedLedger.counts);
@@ -405,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-BlKS5cQQ.css']); assert.deepEqual(assets, ['index-DUSJhcRO.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, 136693); assert.equal(built.byteLength, 133278);
assert.equal(sha256(built), '3486f1e09a97e1293d7cdd4c7661b994bda1f388c1560518657da1b3a8318d6e'); assert.equal(sha256(built), 'f3116a2f0f282a10030589057ec0c62800640b7b552180dfe12d4b737a5fc436');
}); });