From 804e08727e40402eafe259f790b44f762aa05167 Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Sat, 15 Aug 2026 02:26:02 +0300 Subject: [PATCH] Refine client rail icons and motion --- src/shared/versions.ts | 4 +- src/web/features/devices/DevicesFeature.tsx | 12 +- .../diagnostics/DiagnosticsFeature.tsx | 8 +- src/web/features/routing/RoutingFeature.tsx | 2 + .../subscription/SubscriptionFeature.tsx | 7 +- src/web/styles/primitives.css | 140 ++++++------------ src/web/styles/themes.css | 4 + test/web/responsive-layout-contract.test.js | 41 +++-- test/web/style-boundaries.test.js | 34 ++--- 9 files changed, 107 insertions(+), 145 deletions(-) diff --git a/src/shared/versions.ts b/src/shared/versions.ts index e1aabe6..01161f0 100644 --- a/src/shared/versions.ts +++ b/src/shared/versions.ts @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.25.13', - gatewayClient: '0.26.12', + macClient: '0.25.14', + gatewayClient: '0.26.13', gatewayBackend: '0.26.5', }); diff --git a/src/web/features/devices/DevicesFeature.tsx b/src/web/features/devices/DevicesFeature.tsx index 6cf7a9d..6d62ac5 100644 --- a/src/web/features/devices/DevicesFeature.tsx +++ b/src/web/features/devices/DevicesFeature.tsx @@ -243,11 +243,13 @@ export function DevicesToggle({ onClick={onToggle} > ; } diff --git a/src/web/features/diagnostics/DiagnosticsFeature.tsx b/src/web/features/diagnostics/DiagnosticsFeature.tsx index 2e1a48e..df3a00e 100644 --- a/src/web/features/diagnostics/DiagnosticsFeature.tsx +++ b/src/web/features/diagnostics/DiagnosticsFeature.tsx @@ -60,8 +60,12 @@ export function DiagnosticsToggle({ onClick={onToggle} > ; } diff --git a/src/web/features/routing/RoutingFeature.tsx b/src/web/features/routing/RoutingFeature.tsx index a69ed65..543bf89 100644 --- a/src/web/features/routing/RoutingFeature.tsx +++ b/src/web/features/routing/RoutingFeature.tsx @@ -416,6 +416,8 @@ export function RoutingToggle({ > diff --git a/src/web/features/subscription/SubscriptionFeature.tsx b/src/web/features/subscription/SubscriptionFeature.tsx index f86fe20..3c866ae 100644 --- a/src/web/features/subscription/SubscriptionFeature.tsx +++ b/src/web/features/subscription/SubscriptionFeature.tsx @@ -318,10 +318,11 @@ export function SubscriptionToggle({ onClick={onToggle} > ; diff --git a/src/web/styles/primitives.css b/src/web/styles/primitives.css index 21c1c21..a22b611 100644 --- a/src/web/styles/primitives.css +++ b/src/web/styles/primitives.css @@ -8,6 +8,7 @@ } .client-rail-action { + --client-rail-state-duration: 340ms; position: relative; width: 42px; height: 48px; @@ -26,11 +27,16 @@ height: 23px; fill: none; stroke: currentColor; - stroke-width: 1.6; + stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; overflow: visible; - 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 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 { @@ -70,21 +76,16 @@ .client-rail-subscription-back, .client-rail-subscription-front, -.client-rail-subscription-lines, .client-rail-info-ring, -.client-rail-info-sweep, .client-rail-info-mark, -.client-rail-device-primary, -.client-rail-device-secondary, -.client-rail-device-link, +.client-rail-device-monitor, +.client-rail-device-phone, .client-rail-device-bridge, -.client-rail-device-pulse, -.client-rail-diagnostics-base, -.client-rail-diagnostics-pulse, +.client-rail-diagnostics-glass, .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-dasharray 620ms cubic-bezier(0.16, 1, 0.3, 1), stroke-dashoffset 620ms cubic-bezier(0.16, 1, 0.3, 1); + 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-action .client-rail-subscription-card, @@ -92,53 +93,29 @@ fill: var(--client-bg); } -.client-rail-subscription-back { - transform: translate(0, 0); -} - .client-rail-subscription-lines { - opacity: 0.9; - transform-origin: left center; + opacity: 1; } .client-subscription-toggle.is-open .client-rail-subscription-back { - transform: translate(0.6px, -0.6px); + transform: translate(1.5px, -1px); } .client-subscription-toggle.is-open .client-rail-subscription-front { - transform: translate(-1.5px, 1px); -} - -.client-subscription-toggle.is-open .client-rail-subscription-lines { - opacity: 1; - animation: client-rail-detail-pulse 10s ease-in-out infinite; + transform: translate(-2px, 1px); } .client-rail-info-ring { stroke-dasharray: 1 0; - stroke-dashoffset: 0.25; -} - -.client-rail-info-sweep { - stroke-width: 2.4; - stroke-dasharray: 0.14 0.86; - opacity: 0; + stroke-dashoffset: 0.1; } .client-instructions-toggle.is-open .client-rail-info-ring { - stroke-dasharray: 0.78 0.22; + stroke-dasharray: 0.9 0.1; } .client-instructions-toggle.is-open .client-rail-info-mark { - transform: translateY(-0.6px); -} - -.client-instructions-toggle.is-open .client-rail-info-sweep { - animation: client-rail-info-scan 10s linear infinite; -} - -.client-rail-device-link { - opacity: 0.9; + transform: translateY(-1px); } .client-rail-device-bridge { @@ -146,27 +123,27 @@ transform: scaleX(0); } -.client-rail-device-pulse, -.client-rail-diagnostics-pulse { +.client-rail-subscription-scan, +.client-rail-info-sweep, +.client-rail-device-packet, +.client-rail-diagnostics-scan, +.client-rail-rule-scan { stroke-width: 2.4; opacity: 0; - stroke-dasharray: 0.16 0.84; + stroke-dasharray: 0.18 0.82; + stroke-dashoffset: 1; } -.client-rail-device-pulse { - stroke-dasharray: 0.5 0.5; +.client-rail-device-packet { + stroke-dasharray: 0.36 0.64; } -.client-devices-toggle.is-open .client-rail-device-primary { - transform: translateX(0.6px); +.client-devices-toggle.is-open .client-rail-device-monitor { + transform: translateX(1px); } -.client-devices-toggle.is-open .client-rail-device-secondary { - transform: translateX(-0.6px); -} - -.client-devices-toggle.is-open .client-rail-device-link { - opacity: 1; +.client-devices-toggle.is-open .client-rail-device-phone { + transform: translateX(-1px); } .client-devices-toggle.is-open .client-rail-device-bridge { @@ -174,23 +151,8 @@ transform: scaleX(1); } -.client-devices-toggle.is-open .client-rail-device-pulse { - animation: client-rail-device-signal 10s linear infinite; -} - -.client-rail-diagnostics-base, -.client-rail-diagnostics-pulse { - vector-effect: non-scaling-stroke; - transform: scaleY(0.8); -} - -.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-scan 10s linear infinite; +.client-diagnostics-toggle.is-open .client-rail-diagnostics-glass { + transform: translate(1px, -1px) rotate(-4deg); } .client-local-rules-toggle.is-open .client-rail-rule-knob.is-top { @@ -201,38 +163,25 @@ transform: translateX(-2px); } -.client-local-rules-toggle.is-open .client-rail-rule-knob { - animation: client-rail-rule-pulse 10s ease-in-out infinite; +.client-subscription-toggle.is-open .client-rail-subscription-scan, +.client-instructions-toggle.is-open .client-rail-info-sweep, +.client-devices-toggle.is-open .client-rail-device-packet, +.client-diagnostics-toggle.is-open .client-rail-diagnostics-scan, +.client-local-rules-toggle.is-open .client-rail-rule-scan { + animation: client-rail-ambient-trace 10s 1.1s linear infinite; } -@keyframes client-rail-detail-pulse { - 0%, 8%, 100% { transform: translateX(0); } - 4% { transform: translateX(0.8px); } +.client-local-rules-toggle.is-open .client-rail-rule-scan.is-bottom { + animation: client-rail-ambient-trace 10s 1.32s linear infinite; } -@keyframes client-rail-info-scan { +@keyframes client-rail-ambient-trace { 0% { opacity: 0; stroke-dashoffset: 1; } - 2%, 8% { opacity: 1; } + 2% { opacity: 1; } + 8% { opacity: 1; stroke-dashoffset: 0; } 10%, 100% { opacity: 0; stroke-dashoffset: 0; } } -@keyframes client-rail-device-signal { - 0% { opacity: 0; stroke-dashoffset: 1; } - 2%, 8% { opacity: 1; } - 10%, 100% { opacity: 0; stroke-dashoffset: 0; } -} - -@keyframes client-rail-diagnostics-scan { - 0% { opacity: 0; stroke-dashoffset: 1; } - 2%, 8% { opacity: 1; } - 10%, 100% { opacity: 0; stroke-dashoffset: 0; } -} - -@keyframes client-rail-rule-pulse { - 0%, 10%, 100% { stroke-width: 1.6; } - 5% { stroke-width: 2.4; } -} - .client-local-rules-toggle:disabled { cursor: default; color: var(--client-muted); @@ -252,6 +201,7 @@ } .client-rail-action.is-open { + --client-rail-state-duration: 480ms; color: var(--client-accent); } diff --git a/src/web/styles/themes.css b/src/web/styles/themes.css index 3cdefaf..8fa3d12 100644 --- a/src/web/styles/themes.css +++ b/src/web/styles/themes.css @@ -117,6 +117,10 @@ stroke-dashoffset: 0; } + .client-rail-action:active:not(:disabled) svg { + transform: none; + } + .client-local-rule, .client-local-rule-enabled svg, .client-local-rule-enabled circle, diff --git a/test/web/responsive-layout-contract.test.js b/test/web/responsive-layout-contract.test.js index f81ee69..d0b7c8c 100644 --- a/test/web/responsive-layout-contract.test.js +++ b/test/web/responsive-layout-contract.test.js @@ -146,34 +146,32 @@ test('secondary menus share one right rail and switch equal drawers as a vertica assert.match(disabledRulesLabel, /opacity:\s*0/); 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(subscription, /client-rail-subscription-back[\s\S]*client-rail-subscription-front[\s\S]*client-rail-subscription-lines/); + 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(instructions, /client-rail-info-ring[\s\S]*client-rail-info-sweep[\s\S]*client-rail-info-mark/); assert.match(component, / { assert.match(reducedMotion, /\.client-drawer/); assert.match(reducedMotion, /transition:\s*none/); assert.match(reducedMotion, /animation:\s*none/); + assert.match(reducedMotion, /\.client-rail-action:active:not\(:disabled\) svg[\s\S]*transform:\s*none/); }); test('tooltips stay opaque, above adjacent content, and do not stick after pointer clicks', () => { diff --git a/test/web/style-boundaries.test.js b/test/web/style-boundaries.test.js index adb0f2a..976c741 100644 --- a/test/web/style-boundaries.test.js +++ b/test/web/style-boundaries.test.js @@ -38,25 +38,25 @@ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex' const acceptedLedger = { counts: { cascadeEdges: 823, - customProperties: 103, - declarations: 3333, + customProperties: 105, + declarations: 3314, important: 0, - keyframes: 55, + keyframes: 51, media: 12, - rules: 961, - variableReferences: 811, + rules: 946, + variableReferences: 814, }, hashes: { - cascadeEdges: '20d8763bd907f8182994186e2c53e03885ecdcb8dda07c69ff7e64b5ac808625', - customProperties: 'ef70fb1d9b61b177f1939f811babe1116bee631de26f5eac0aa0d0009ca5a1fb', - declarations: '633264a595c8ade20cae3adb50bb484c61c22f430b7760155efb4ec4b15d30de', + cascadeEdges: '42851684f732dd5e7f04959d009648dd4a8fab85904759da16786411ab199d1a', + customProperties: 'dede9598ee62929fb29ff470897cd475ca34eb5af5dda342b09e306f3eaee887', + declarations: 'fac169725174c1303de5673abfd42b7316c2180830d66173dca8a2e449b1db5e', duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', duplicateSelectors: '0fa66e370695261a2a5c6a189752c07d518d80d68d3e79a3ba4bb34407893c0e', - keyframes: '35e40cc917b4c0d5a99bb0a3a626736e134167d29cf303ec41d02bf713d18637', - ruleDeclarationSequences: 'b7643608ce8526684769dc22bf6641962e324093f9f20b6239e7d71dab031272', - selectors: '896dc29ad2ed823b0754d4c667c093c5733bc54af951cf2b9ed83e9c022836d5', - variableReferences: '0ffb99792db35b451fb35d1ced24e1aeef3a6df769cb37bf3d61229b0c970eba', - witnesses: 'f9c177d5f967c116a1f8a79c05a6d1faf1231775045dbafbc1772d2f916e40e0', + keyframes: 'b2b826683e11dfa95cf2d01996d43a02b3a2b33672a14353ebfbee7701d4532e', + ruleDeclarationSequences: '7750298af4530d2b0f7d7b0feee21c6bd00a5f9a7ed1d6cfa368e2d9bcace26c', + selectors: 'aa487c004e9960e60982e983fcbdb3becef76b0007d3ef85a96f45d4c3d7933e', + variableReferences: '67fbd84828e97d10d7fe163ebea6504e8c48355e99616ede2498307d836fa616', + witnesses: '5ae376bbd8884cc14a6907fc94302e0cd34974bf0b5beeb66dda1978a23d1959', }, }; @@ -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', () => { const witnesses = readStyleWitnesses(root); - assert.equal(witnesses.length, 817); + assert.equal(witnesses.length, 824); assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0); const ledger = createStyleLedger(readStyleSource(root), { witnesses }); 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); const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css')); - assert.deepEqual(assets, ['index-Ceg2DPeq.css']); + assert.deepEqual(assets, ['index-DUedU53N.css']); const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0])); - assert.equal(built.byteLength, 129584); - assert.equal(sha256(built), '8ccbb067a01c28e729a1aa34908a96e508f07fb9e12c0d05ead10ddc959731bc'); + assert.equal(built.byteLength, 128983); + assert.equal(sha256(built), 'eeeb9cc0bebc7afc37c2d6e60808aa4980f91c564762e47d240c654dd1b00fcf'); });