diff --git a/src/shared/versions.ts b/src/shared/versions.ts index 648b4c8..e2867dd 100644 --- a/src/shared/versions.ts +++ b/src/shared/versions.ts @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.25.11', - gatewayClient: '0.26.10', + macClient: '0.25.12', + gatewayClient: '0.26.11', gatewayBackend: '0.26.5', }); diff --git a/src/web/components/ClientOverviewPage.tsx b/src/web/components/ClientOverviewPage.tsx index f3512c9..75c5057 100644 --- a/src/web/components/ClientOverviewPage.tsx +++ b/src/web/components/ClientOverviewPage.tsx @@ -445,6 +445,7 @@ export function ClientOverviewPage({ const [showIntro, setShowIntro] = useState(true); const [copyFeedback, setCopyFeedback] = useState({}); const [copyAnnouncement, setCopyAnnouncement] = useState({ text: '', cycle: 0 }); + const [drawerSwitchTarget, setDrawerSwitchTarget] = useState(null); const copyTimersRef = useRef>>>({}); const copyAttemptsRef = useRef>>({}); 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({ > switchDrawer('subscription')} /> switchDrawer('instructions')} /> {isGateway && switchDrawer('devices')} />} switchDrawer('diagnostics')} /> ; -export function DevicesToggle({ feature, onToggle }: { feature: DevicesFeature; onToggle: () => void }) { +export function DevicesToggle({ + feature, + open, + onToggle, +}: { + feature: DevicesFeature; + open: boolean; + onToggle: () => void; +}) { return ; } diff --git a/src/web/features/diagnostics/DiagnosticsFeature.tsx b/src/web/features/diagnostics/DiagnosticsFeature.tsx index b2c8069..2e1a48e 100644 --- a/src/web/features/diagnostics/DiagnosticsFeature.tsx +++ b/src/web/features/diagnostics/DiagnosticsFeature.tsx @@ -43,17 +43,19 @@ export type DiagnosticsFeature = ReturnType; export function DiagnosticsToggle({ feature, + open, onToggle, }: { feature: DiagnosticsFeature; + open: boolean; onToggle: () => void; }) { return diff --git a/src/web/features/instructions/InstructionsFeature.tsx b/src/web/features/instructions/InstructionsFeature.tsx index cbfedfc..a044e0c 100644 --- a/src/web/features/instructions/InstructionsFeature.tsx +++ b/src/web/features/instructions/InstructionsFeature.tsx @@ -232,23 +232,26 @@ export type InstructionsFeature = ReturnType; export function InstructionsToggle({ feature, + open, onToggle, }: { feature: InstructionsFeature; + open: boolean; onToggle: () => void; }) { return ; } diff --git a/src/web/features/routing/RoutingFeature.tsx b/src/web/features/routing/RoutingFeature.tsx index 5f708e0..30f4501 100644 --- a/src/web/features/routing/RoutingFeature.tsx +++ b/src/web/features/routing/RoutingFeature.tsx @@ -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({ feature.isOpen ? feature.requestClose() : onOpen()} + onClick={() => open ? feature.requestClose() : onOpen()} > ); diff --git a/src/web/features/subscription/SubscriptionFeature.tsx b/src/web/features/subscription/SubscriptionFeature.tsx index 16c98ed..f86fe20 100644 --- a/src/web/features/subscription/SubscriptionFeature.tsx +++ b/src/web/features/subscription/SubscriptionFeature.tsx @@ -301,22 +301,28 @@ type SubscriptionFeatureController = ReturnType; export function SubscriptionToggle({ feature, + open, onToggle, }: { feature: SubscriptionFeatureController; + open: boolean; onToggle: () => void; }) { return ; } diff --git a/src/web/styles/primitives.css b/src/web/styles/primitives.css index dcb2ce6..0fb03e4 100644 --- a/src/web/styles/primitives.css +++ b/src/web/styles/primitives.css @@ -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 { diff --git a/src/web/styles/themes.css b/src/web/styles/themes.css index 2bd3a1e..3cdefaf 100644 --- a/src/web/styles/themes.css +++ b/src/web/styles/themes.css @@ -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, diff --git a/test/web/responsive-layout-contract.test.js b/test/web/responsive-layout-contract.test.js index 5d6c44a..c6e3bf9 100644 --- a/test/web/responsive-layout-contract.test.js +++ b/test/web/responsive-layout-contract.test.js @@ -146,17 +146,26 @@ 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(instructions, /client-rail-info-ring/); + 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 is-left[\s\S]*client-rail-book-page is-right[\s\S]*client-rail-book-spine/); assert.match(component, /\(null\)/); + assert.match(component, /const activeRailDrawer = drawerSwitchTarget && drawerControls\[drawerSwitchTarget\]\.isOpen[\s\S]*drawerControls\[drawer\]\.isOpen/); + assert.match(component, /setDrawerSwitchTarget\(target\);[\s\S]*flushSync\(\(\) => toControl\.show\(\)\)/); + assert.match(component, /fromControl\.close\(\);[\s\S]*setDrawerSwitchTarget\(null\)/); + assert.match(component, / \{[\s\S]*from\.inert = false;[\s\S]*from\.removeAttribute\('aria-hidden'\)/); assert.match(component, /DRAWER_SWITCH_MS = 620[\s\S]*prefers-reduced-motion: reduce[\s\S]*flushSync/); assert.match(component, /current === 'routing' && routingFeature\.dirty[\s\S]*routingFeature\.requestClose\(\)/); @@ -230,6 +244,7 @@ test('responsive motion has a reduced-motion fallback', () => { assert.match(reducedMotion, /\.client-form/); assert.match(reducedMotion, /\.harbor-brand/); assert.match(reducedMotion, /\.client-rail-action circle/); + assert.match(reducedMotion, /\.client-rail-action g/); assert.match(reducedMotion, /\.client-rail-action rect/); assert.match(reducedMotion, /\.client-drawer/); assert.match(reducedMotion, /transition:\s*none/); diff --git a/test/web/style-boundaries.test.js b/test/web/style-boundaries.test.js index 7a460ee..f54cb64 100644 --- a/test/web/style-boundaries.test.js +++ b/test/web/style-boundaries.test.js @@ -37,26 +37,26 @@ const expectedImports = [ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex'); const acceptedLedger = { counts: { - cascadeEdges: 821, + cascadeEdges: 822, customProperties: 103, - declarations: 3308, + declarations: 3325, important: 0, - keyframes: 56, + keyframes: 52, media: 12, - rules: 947, - variableReferences: 810, + rules: 953, + variableReferences: 811, }, hashes: { - cascadeEdges: '54abf37aa617e90947c3e0cfd35f2358f8873bda512992c5628950d828ea7486', + cascadeEdges: 'c9c0e3988cea68ecc1c5e06e12b3b12fe9bb4d20482a67abcf838296ccf9740f', customProperties: 'ef70fb1d9b61b177f1939f811babe1116bee631de26f5eac0aa0d0009ca5a1fb', - declarations: '97e97ccb50010db1950b2a2ceeb3bdbcc8574aaee5a676724d3332a69fab537d', + declarations: '856c8aa1395c435723a52d709a39c9d37e0ede48ddb59f4ce8eff91d695e605d', duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', duplicateSelectors: '0fa66e370695261a2a5c6a189752c07d518d80d68d3e79a3ba4bb34407893c0e', - keyframes: 'd4378751f36eccc87923c12540315462055032a5d34978a0f45ecada0a5cc1ce', - ruleDeclarationSequences: '14f4eceda4a1811799b7dc50491c9298c27c5796fa8fc8ce02104bdb268d7675', - selectors: '1420d737139ad3e03bbdcf5a4e1573443e500cda572b4461bdc578cd6b3c04de', - variableReferences: 'c8e01fce4a7f3a976040f20938ec8d70403bd003f66ef13b5637c77f9c4cef08', - witnesses: 'b47da99c00da77a5c9716796304fab2163fb21c6bc6753176f008f85a3420695', + keyframes: 'd3050d026f6fba3975cf6bb07320d0c3f400c3b3b6eaa3cecbc53b0b137083a3', + ruleDeclarationSequences: 'e129c791ddbc3fa3f28fe4fb5565688a7e2e212eff2f8f8e85d76394f9628847', + selectors: '4ab0b3b0944d13512d5ea28255d031b4b2cb7469883057e8f16ab090580fbd51', + variableReferences: '0ffb99792db35b451fb35d1ced24e1aeef3a6df769cb37bf3d61229b0c970eba', + witnesses: 'c502e252880f18c633b82d09bf349de134c52da4b67a8dfadd327ee01b464692', }, }; @@ -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, 811); + assert.equal(witnesses.length, 817); 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-0HVoD6xZ.css']); + assert.deepEqual(assets, ['index-D5lRz6FV.css']); const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0])); - assert.equal(built.byteLength, 127874); - assert.equal(sha256(built), '97241721abba48f5a120e08c425fb67b8b3ab560e59b479351ea455837838805'); + assert.equal(built.byteLength, 129198); + assert.equal(sha256(built), '1108b32f6d493f170b7683a1947c248c5d4e1295dd810bdc0f0935d133c28497'); });