From 32217f4d17e84757dbe5d62310066e7e9e670f41 Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Fri, 14 Aug 2026 15:23:00 +0300 Subject: [PATCH] Animate vertical transitions between client drawers --- .../design-harbor-device-ecosystem/SKILL.md | 85 +++++++++ .../agents/openai.yaml | 4 + src/shared/versions.ts | 4 +- src/web/components/ClientOverviewPage.tsx | 161 ++++++++++++++---- src/web/styles/features/devices.css | 8 +- src/web/styles/features/diagnostics.css | 4 - src/web/styles/features/instructions.css | 4 - src/web/styles/features/routing.css | 4 - src/web/styles/features/servers.css | 1 + src/web/styles/features/subscription.css | 9 +- src/web/styles/primitives.css | 13 +- test/web/device-inventory-contract.test.js | 3 +- test/web/devices-feature-contract.test.js | 2 +- test/web/responsive-layout-contract.test.js | 22 ++- test/web/routing-feature-contract.test.js | 4 +- test/web/style-boundaries.test.js | 20 +-- .../web/subscription-feature-contract.test.js | 5 +- 17 files changed, 260 insertions(+), 93 deletions(-) create mode 100644 .codex/skills/design-harbor-device-ecosystem/SKILL.md create mode 100644 .codex/skills/design-harbor-device-ecosystem/agents/openai.yaml diff --git a/.codex/skills/design-harbor-device-ecosystem/SKILL.md b/.codex/skills/design-harbor-device-ecosystem/SKILL.md new file mode 100644 index 0000000..7e0a8fc --- /dev/null +++ b/.codex/skills/design-harbor-device-ecosystem/SKILL.md @@ -0,0 +1,85 @@ +--- +name: design-harbor-device-ecosystem +description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.] +--- + +# Design Harbor Device Ecosystem + +## Overview + +[TODO: 1-2 sentences explaining what this skill enables] + +## Structuring This Skill + +[TODO: Choose the structure that best fits this skill's purpose. Common patterns: + +**1. Workflow-Based** (best for sequential processes) +- Works well when there are clear step-by-step procedures +- Example: DOCX skill with "Workflow Decision Tree" -> "Reading" -> "Creating" -> "Editing" +- Structure: ## Overview -> ## Workflow Decision Tree -> ## Step 1 -> ## Step 2... + +**2. Task-Based** (best for tool collections) +- Works well when the skill offers different operations/capabilities +- Example: PDF skill with "Quick Start" -> "Merge PDFs" -> "Split PDFs" -> "Extract Text" +- Structure: ## Overview -> ## Quick Start -> ## Task Category 1 -> ## Task Category 2... + +**3. Reference/Guidelines** (best for standards or specifications) +- Works well for brand guidelines, coding standards, or requirements +- Example: Brand styling with "Brand Guidelines" -> "Colors" -> "Typography" -> "Features" +- Structure: ## Overview -> ## Guidelines -> ## Specifications -> ## Usage... + +**4. Capabilities-Based** (best for integrated systems) +- Works well when the skill provides multiple interrelated features +- Example: Product Management with "Core Capabilities" -> numbered capability list +- Structure: ## Overview -> ## Core Capabilities -> ### 1. Feature -> ### 2. Feature... + +Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations). + +Delete this entire "Structuring This Skill" section when done - it's just guidance.] + +## [TODO: Replace with the first main section based on chosen structure] + +[TODO: Add content here. See examples in existing skills: +- Code samples for technical skills +- Decision trees for complex workflows +- Concrete examples with realistic user requests +- References to scripts/templates/references as needed] + +## Resources (optional) + +Create only the resource directories this skill actually needs. Delete this section if no resources are required. + +### scripts/ +Executable code (Python/Bash/etc.) that can be run directly to perform specific operations. + +**Examples from other skills:** +- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation +- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing + +**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations. + +**Note:** Scripts may be executed without loading into context, but can still be read by Codex for patching or environment adjustments. + +### references/ +Documentation and reference material intended to be loaded into context to inform Codex's process and thinking. + +**Examples from other skills:** +- Product management: `communication.md`, `context_building.md` - detailed workflow guides +- BigQuery: API reference documentation and query examples +- Finance: Schema documentation, company policies + +**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Codex should reference while working. + +### assets/ +Files not intended to be loaded into context, but rather used within the output Codex produces. + +**Examples from other skills:** +- Brand styling: PowerPoint template files (.pptx), logo files +- Frontend builder: HTML/React boilerplate project directories +- Typography: Font files (.ttf, .woff2) + +**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output. + +--- + +**Not every skill requires all three types of resources.** diff --git a/.codex/skills/design-harbor-device-ecosystem/agents/openai.yaml b/.codex/skills/design-harbor-device-ecosystem/agents/openai.yaml new file mode 100644 index 0000000..e42f34b --- /dev/null +++ b/.codex/skills/design-harbor-device-ecosystem/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Design Harbor Device Ecosystem" + short_description: "Design trusted Harbor device integration." + default_prompt: "Use -harbor-device-ecosystem to design Harbor Gateway discovery, pairing, and connected-device contracts." diff --git a/src/shared/versions.ts b/src/shared/versions.ts index 4c82850..9283d34 100644 --- a/src/shared/versions.ts +++ b/src/shared/versions.ts @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.25.9', - gatewayClient: '0.26.8', + macClient: '0.25.10', + gatewayClient: '0.26.9', gatewayBackend: '0.26.5', }); diff --git a/src/web/components/ClientOverviewPage.tsx b/src/web/components/ClientOverviewPage.tsx index a24db81..f3512c9 100644 --- a/src/web/components/ClientOverviewPage.tsx +++ b/src/web/components/ClientOverviewPage.tsx @@ -5,6 +5,7 @@ import React, { useState, type CSSProperties, } from 'react'; +import { flushSync } from 'react-dom'; import { copyText, localProxyUrls, @@ -63,6 +64,10 @@ const VERSION_PARTS = [ ['hotfix', 'Hotfix'], ] as const; +const DRAWER_SWITCH_MS = 620; +const DRAWER_ORDER = ['subscription', 'instructions', 'devices', 'diagnostics', 'routing'] as const; +type DrawerKey = typeof DRAWER_ORDER[number]; + interface UiError { context?: string; profileId?: string; @@ -442,6 +447,10 @@ export function ClientOverviewPage({ const [copyAnnouncement, setCopyAnnouncement] = useState({ text: '', cycle: 0 }); const copyTimersRef = useRef>>>({}); const copyAttemptsRef = useRef>>({}); + const drawerSwitchRef = useRef<{ + target: DrawerKey; + finish: () => void; + } | null>(null); const gatewayAddress = isGateway ? window.location.hostname : '127.0.0.1'; const controlHost = window.location.host || `${gatewayAddress}:3456`; const proxyUrls = localProxyUrls(state?.clientRuntime?.proxyPort, gatewayAddress); @@ -523,6 +532,41 @@ export function ClientOverviewPage({ controlHost, }); const diagnosticsAvailable = hasSubscription; + const drawerControls = { + subscription: { + isOpen: subscriptionFeature.open, + panelRef: subscriptionFeature.panelRef, + show: subscriptionFeature.toggle, + close: subscriptionFeature.close, + }, + instructions: { + isOpen: instructionsFeature.isOpen, + panelRef: instructionsFeature.panelRef, + show: instructionsFeature.toggle, + close: instructionsFeature.close, + }, + devices: { + isOpen: devicesFeature.isOpen, + panelRef: devicesFeature.panelRef, + show: devicesFeature.toggle, + close: devicesFeature.close, + }, + diagnostics: { + isOpen: diagnosticsFeature.isOpen, + panelRef: diagnosticsFeature.panelRef, + show: diagnosticsFeature.toggle, + close: diagnosticsFeature.close, + }, + routing: { + isOpen: routingFeature.isOpen, + panelRef: routingFeature.panelRef, + show: routingFeature.open, + close: routingFeature.forceClose, + }, + }; + const drawerOrder = isGateway + ? DRAWER_ORDER + : DRAWER_ORDER.filter((drawer) => drawer !== 'devices'); useEffect(() => { setNow(Date.now()); @@ -598,12 +642,77 @@ export function ClientOverviewPage({ }, 800); } - function openRouting() { - subscriptionFeature.close(); - instructionsFeature.close(); - devicesFeature.close(); - diagnosticsFeature.close(); - routingFeature.open(); + function switchDrawer(target: DrawerKey) { + const activeSwitch = drawerSwitchRef.current; + const current = activeSwitch?.target + || drawerOrder.find((drawer) => drawerControls[drawer].isOpen) + || null; + activeSwitch?.finish(); + + if (current === target) { + drawerControls[target].close(); + return; + } + if (current === 'routing' && routingFeature.dirty) { + routingFeature.requestClose(); + return; + } + if (!current) { + drawerControls[target].show(); + return; + } + + const fromControl = drawerControls[current]; + const toControl = drawerControls[target]; + const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; + if (reducedMotion) { + flushSync(() => { + fromControl.close(); + toControl.show(); + }); + return; + } + + 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(); + return; + } + + from.inert = true; + from.setAttribute('aria-hidden', 'true'); + const direction = DRAWER_ORDER.indexOf(target) > DRAWER_ORDER.indexOf(current) ? -1 : 1; + const options: KeyframeAnimationOptions = { + duration: DRAWER_SWITCH_MS, + easing: 'cubic-bezier(0.16, 1, 0.3, 1)', + fill: 'both', + }; + const outgoing = from.animate([ + { transform: 'translateY(0)', opacity: 1 }, + { transform: `translateY(${direction * 100}%)`, opacity: 1 }, + ], options); + const incoming = to.animate([ + { transform: `translateY(${-direction * 100}%)`, opacity: 1 }, + { transform: 'translateY(0)', opacity: 1 }, + ], options); + let finished = false; + const cancel = () => { + from.inert = false; + from.removeAttribute('aria-hidden'); + outgoing.cancel(); + incoming.cancel(); + }; + const finish = () => { + if (finished) return; + finished = true; + flushSync(() => fromControl.close()); + cancel(); + if (drawerSwitchRef.current?.target === target) drawerSwitchRef.current = null; + }; + incoming.addEventListener('finish', finish, { once: true }); + drawerSwitchRef.current = { target, finish }; } const mainIdentity = gatewayDirect @@ -629,53 +738,33 @@ export function ClientOverviewPage({
{copyAnnouncement.text}
- {hasSubscription &&