diff --git a/.agent/skills/react-typescript-ui/SKILL.md b/.agent/skills/react-typescript-ui/SKILL.md index 305d8f8..c1c012b 100644 --- a/.agent/skills/react-typescript-ui/SKILL.md +++ b/.agent/skills/react-typescript-ui/SKILL.md @@ -47,6 +47,21 @@ src/app/components/SingBoxWorkspace.tsx - Use `aria-*` for tabs, toggle buttons, popovers, service controls. - Honor reduced motion where relevant. +## Animated disclosures + +- Keep disclosure content mounted through opening and closing so both directions can animate. Do not conditionally render content directly into its final open state. +- Keep the trigger at one screen position and separate layout placement from hover/active transforms. +- Gate hidden content with `aria-hidden` plus `inert` or `tabIndex`; opacity and `pointer-events` alone do not remove controls from keyboard navigation. +- Keep `aria-expanded` and `aria-controls` on the trigger synchronized with the rendered state. +- Use one motion origin and timeline for background, copy, and actions. Implement and verify the reverse transition at the same time as the entrance. + +## Startup responsiveness + +- Render the shell and saved/default configuration immediately. Do not gate first paint on network access, subscription refresh, or every component probe. +- Show slow component detection in reserved `checking` geometry and apply partial results as they arrive without replaying page entrance motion. +- Do not serialize independent probes to create a staged UI. When the backend exposes only an aggregate snapshot, animate reserved placeholders and replace their values in place when that snapshot arrives. +- Keep navigation and already-known configuration usable while background detection continues. + ## Proxy/routing UI When editing route UI: @@ -71,6 +86,7 @@ Recommended for extracted pure logic: - Unit tests for readiness states. - Unit tests for snapshot diff/change dock model. - UI smoke checks for desktop and narrow layout. +- For hover, disclosure, or motion changes, exercise first open, close, repeated toggle, hover during transition, keyboard focus, loading copy, and `prefers-reduced-motion`. Build and unit tests do not validate these behaviors. ## Do not diff --git a/.agent/skills/rust-tauri-backend/SKILL.md b/.agent/skills/rust-tauri-backend/SKILL.md index bec7d5c..7219573 100644 --- a/.agent/skills/rust-tauri-backend/SKILL.md +++ b/.agent/skills/rust-tauri-backend/SKILL.md @@ -32,6 +32,14 @@ pub async fn some_command(input: SomeInput) -> Result Implementation function должна быть тестируемой без Tauri runtime, если возможно. +## Startup responsiveness + +- Keep first paint independent from network access, subscription refresh, and slow component detection. +- Run independent startup probes concurrently and outside the async runtime thread. Do not serialize ProxiFyre, sing-box, service, and admin checks without a dependency between them. +- Give external or process-heavy probes a bounded timeout and return partial status when one probe is slow or unavailable. +- Load saved configuration and other cheap state first. Let the UI render it while detection results update separately or through a partial startup snapshot. +- Do not fail the entire startup snapshot because one optional component cannot be detected. Preserve structured per-component errors or unknown/checking state. + ## DTO boundary При добавлении или изменении command: @@ -89,6 +97,7 @@ services/singbox_service.rs - `cargo clippy --all-targets --all-features -- -D warnings` - `cargo test --all-targets` - Relevant Windows/manual check if touching service/install/elevation. +- When changing startup aggregation, verify that one delayed or failed probe does not postpone unrelated saved state or component results. Если `cargo` недоступен в среде, честно написать, что backend проверен только статически. Не изображать компилятор, у него и так тяжелая жизнь. diff --git a/.agent/skills/testing-ci-release/SKILL.md b/.agent/skills/testing-ci-release/SKILL.md index 3351cb6..059d5e1 100644 --- a/.agent/skills/testing-ci-release/SKILL.md +++ b/.agent/skills/testing-ci-release/SKILL.md @@ -37,6 +37,20 @@ PowerShell plan-only: & .\scripts\install-singbox.ps1 -PlanOnly ``` +## Interaction smoke for UI motion + +Build, lint, and unit tests do not validate motion or pointer behavior. For any hover, disclosure, stagger, or hit-target change, verify: + +- first open and first close; +- repeated and rapid toggle; +- hover and click before, during, and after transition; +- keyboard focus and hidden-control tab order; +- loading and longest localized labels; +- `prefers-reduced-motion`; +- desktop and narrow window geometry. + +Use a controlled mock or preview state when backend status is difficult to reproduce. If no visual interaction smoke is possible, report that evidence as missing and do not claim the motion task is complete. + ## CI recommendation Add GitHub Actions with at least: diff --git a/.codex/skills/design-proxywarden-ui/SKILL.md b/.codex/skills/design-proxywarden-ui/SKILL.md index b22ef68..ffd32a4 100644 --- a/.codex/skills/design-proxywarden-ui/SKILL.md +++ b/.codex/skills/design-proxywarden-ui/SKILL.md @@ -12,10 +12,12 @@ Keep ProxyWarden a compact Windows utility while matching the visual language of 1. Read `AGENTS.md`, `.agent/skills/react-typescript-ui/SKILL.md`, and the complete component and CSS being changed. 2. Read [visual-language.md](references/visual-language.md) for composition, typography, color, and surfaces. 3. Read [motion-and-interaction.md](references/motion-and-interaction.md) for state and interaction animation. -4. Reuse `src/ui/*`, existing state, CSS tokens, and typed Tauri boundaries. Prefer CSS and narrow markup changes over dependencies or new abstractions. -5. Keep geometry stable across loading, success, error, copy, refresh, and route changes. -6. Add `prefers-reduced-motion` behavior with every new animation. -7. Run `npm test`, `npm run build`, and a visual desktop/narrow smoke when the environment allows it. +4. Before editing a disclosure or motion-heavy control, write a compact storyboard for `collapsed`, `opening`, `open`, and `closing`: fixed elements, origin, direction, duration, easing, focus, and reduced-motion behavior. +5. Reuse `src/ui/*`, existing state, CSS tokens, and typed Tauri boundaries. Prefer CSS and narrow markup changes over dependencies or new abstractions. +6. Keep geometry stable across loading, success, error, copy, refresh, and route changes. +7. Add `prefers-reduced-motion` behavior with every new animation. +8. After a second user correction to the same interaction, stop stacking overrides. Re-read its markup and styles, restate the latest behavior, remove superseded assumptions, and rebuild the motion model cleanly. +9. Run `npm test`, `npm run build`, and an interaction smoke for visible motion or hit-target changes. Check desktop and narrow layouts; build and lint never substitute for visual verification. ## Non-negotiable decisions @@ -25,8 +27,10 @@ Keep ProxyWarden a compact Windows utility while matching the visual language of - Use the blue-green accent for ready/active routing and orange only for direct/local-route distinction. Keep warnings and errors semantic. - Prefer open composition, quiet surface shifts, and localized light over dashboard cards, thick borders, and decorative chrome. - Animate opacity, blur, glow, color, filter, and transform; never animate layout properties or use `transition: all`. +- Keep interactive triggers at one screen position throughout disclosure motion. Use layout for resting placement, never a transform that hover or active feedback can overwrite. +- Make transient prompts independent overlays; they must not add shell height or move the main workspace. - Keep labels, paths, status copy, spinners, and feedback in reserved geometry so neighboring content does not move. - Keep tooltips independent from transformed, rotating, glowing, or filtered controls. - Keep secrets and credential-bearing URLs redacted in every visual state. - Keep narrow layouts single-column and keyboard focus visible. - +- Do not call a motion task complete without checking open, close, repeated toggle, hover during transition, keyboard focus, and reduced motion. If the state cannot be reproduced, report the missing visual evidence explicitly. diff --git a/.codex/skills/design-proxywarden-ui/references/motion-and-interaction.md b/.codex/skills/design-proxywarden-ui/references/motion-and-interaction.md index e162269..cbfd351 100644 --- a/.codex/skills/design-proxywarden-ui/references/motion-and-interaction.md +++ b/.codex/skills/design-proxywarden-ui/references/motion-and-interaction.md @@ -28,12 +28,22 @@ Use `cubic-bezier(0.16, 1, 0.3, 1)` for arrivals and interaction feedback. - Keep mode selectors outside the keyed content they replace. Let the new content enter with a short directional fade and blur while focus remains on the selected mode. - For user-triggered sorting, fade and lightly blur the reordered list as one surface; row stagger stays bounded and saved data order does not change. +## Anchored disclosures + +- Keep the trigger fixed while its surface opens and closes. Position its resting hit area with grid, flex, or logical inset properties; never rely on a placement `transform` that hover or active feedback can replace. +- Give the surface, background, copy, and actions one origin and one timeline. They should emerge from the trigger together; do not make the background pop before the trigger or appear after the content. +- Keep animated disclosure content mounted through entry and exit. Gate pointer and keyboard access separately; conditional rendering directly into the final state is not an entrance animation. +- Design opening and closing together. Preserve visible reverse motion long enough before fading opacity, and keep both directions interruptible under repeated clicks. +- Let explicit product feedback override the default easing. When a component calls for a slow start followed by acceleration, define a local curve instead of forcing the global ease-out. +- Compose hover and active feedback without changing the resting position. If transform composition is unavoidable, use separate wrappers, individual transform properties, or shared custom properties and verify every state. +- Keep decorative sweeps subordinate to state motion, low-opacity, bounded to the surface, and finished cleanly. The disclosure must remain legible without the effect. + ## Lists and disclosures - Reveal dynamic rows with opacity, light blur, and a small transform. - On hover, let a row lift one or two pixels and reveal a restrained local surface/light; keep resting rows visually flat. - Animate status dots through color, light, and a small scale change instead of animating a surrounding badge or border. -- Keep departing rows mounted until their exit animation completes; remove immediately under reduced motion. +- Keep departing rows and disclosures mounted until their exit animation completes; remove immediately under reduced motion. - Bound list stagger to 60-100ms and never make interaction latency grow with list length. - Tooltips appear quickly above the trigger as independent translucent surfaces and never inherit trigger transforms or filters. diff --git a/.codex/skills/design-proxywarden-ui/references/visual-language.md b/.codex/skills/design-proxywarden-ui/references/visual-language.md index bd046bd..28644c4 100644 --- a/.codex/skills/design-proxywarden-ui/references/visual-language.md +++ b/.codex/skills/design-proxywarden-ui/references/visual-language.md @@ -40,6 +40,13 @@ Design for a Windows user opening a small control surface to check routing, reco - Prefer a short luminous underline or localized glow for selection and keyboard focus over a rectangular focus frame. - Use shared `src/ui` primitives and preserve their default, hover, active, focus, disabled, loading, empty, and error states. +## Emphasis and border budget + +- Give each compact surface one dominant accent at most. A transient warning action must not outshine the primary system state or its trigger. +- Do not stack borders on the container, trigger, and action. Start with tonal background, spacing, and text hierarchy; keep persistent outlines for keyboard focus, destructive confirmation, or an otherwise ambiguous hit target. +- Treat warm warning color as a restrained semantic tint, not decorative fill or a large glow. Adapt a shared `primary` button locally when its default emphasis conflicts with the surrounding prompt. +- Validate the complete component, not isolated controls: resting, hover, focus, active, disabled, loading, open, and closed states must share one radius and emphasis language. + ## Route checks - Keep the route description, endpoint, and check action in a stable three-part row. Reserve the action width so mode changes and endpoint length never move the button. @@ -48,6 +55,26 @@ Design for a Windows user opening a small control surface to check routing, reco - Keep the summary short. Put verbose URLs, request methods, status codes, and errors in a calm structured detail cloud opened by hovering or focusing the result surface. - Animate result arrival and status light, while preserving the same geometry and honoring reduced motion. +## Route chain semantics + +- Show only stages with distinct user-facing responsibilities. Never render both `Выход` and `SOCKS5 endpoint` when they describe the same destination. +- Use `Приложения → ProxiFyre → SOCKS5` for the external-proxy route. End a direct route with `Интернет: напрямую` instead of an implementation-stage label. +- Explain each stage in plain Russian for a non-technical user. Omit filesystem paths, ports, service names, and generated-config details unless the user explicitly asks for diagnostics. +- Reserve the final chain height before revealing nodes. Progressive arrival may change opacity, blur, or transform, but must not reflow neighboring content. +- Treat progressive arrival as a presentation sequence over reserved slots. Do not serialize independent backend probes just to match the animation; if the API returns one aggregate snapshot, show calm `checking` placeholders and replace them in place. +- Reveal the initial chain in a short, legible sequence and do not replay it for background polling or quiet status refreshes. + +## Admin elevation prompt + +- Render the prompt as a fixed bottom-right overlay that never changes shell height or shifts the workspace. Offset it above persistent bottom docks instead of covering them. +- Keep the collapsed trigger as a stationary 44px warm shield. Show a concise hint once per application session after admin status is known, then dismiss it automatically. +- On click, expand the surface leftward from the shield while the shield stays in the same screen position. Keep the full row height tied to the trigger. +- Reveal background, copy, and action from the same origin and timeline. Use a roughly 520-560ms slow-start opening and a visible 380-420ms reverse close; never delay the background until the end. +- Keep any light pass subtle, local, and optional. It must not replace the actual surface/content motion. +- Use the concise title `Нужны права администратора`, the reason `Для управления ProxiFyre и правилами Windows.`, and the action `Перезапустить`. Do not show paths or elevation internals. +- Keep the surface and action borderless by default. Use a muted warm tint; the action must remain quieter than the shield and main system state. +- Verify the Russian copy, `Открываю UAC`, hover, focus, repeated toggle, narrow width, and Windows text scaling without clipping or layout movement. + ## Route modes and managed lists - Present external and local proxy routes as two peer choices above the content they replace. Keep the chooser mounted while the mode body crossfades in from the selected direction. diff --git a/src-tauri/src/configuration_use_case.rs b/src-tauri/src/configuration_use_case.rs index d591460..3d44d78 100644 --- a/src-tauri/src/configuration_use_case.rs +++ b/src-tauri/src/configuration_use_case.rs @@ -122,8 +122,11 @@ pub fn read_components(storage: &JsonStorage) -> Result, pub fn read_startup_snapshot( storage: &JsonStorage, ) -> Result { - let detected_proxyfier = detect_proxyfier_install(); + // Both detectors query Windows independently. Run them together so the + // startup snapshot is bounded by the slower check instead of their sum. + let proxyfier_detection = std::thread::spawn(detect_proxyfier_install); let detected_singbox = detect_singbox_install(); + let detected_proxyfier = proxyfier_detection.join().ok().flatten(); let saved_state = read_saved_state_with_proxifyre_config( storage, detected_proxyfier diff --git a/src/app/App.tsx b/src/app/App.tsx index f283eb3..be9495a 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -220,6 +220,11 @@ export function App() { const [serviceVisualState, setServiceVisualState] = useState(null); const serviceVisualTimerRef = useRef(null); + const [isAdminPromptOpen, setIsAdminPromptOpen] = useState(false); + const [isAdminPromptHintVisible, setIsAdminPromptHintVisible] = + useState(false); + const adminPromptSeenRef = useRef(false); + const adminPromptTimerRef = useRef(null); const proxyfier = useMemo( () => components.find((component) => component.id === "proxyfier"), @@ -278,11 +283,32 @@ export function App() { void refresh(); }, []); + useEffect(() => { + if (!hasAdminPrompt || adminPromptSeenRef.current) return undefined; + + adminPromptSeenRef.current = true; + setIsAdminPromptHintVisible(true); + adminPromptTimerRef.current = window.setTimeout(() => { + setIsAdminPromptHintVisible(false); + adminPromptTimerRef.current = null; + }, 4600); + + return () => { + if (adminPromptTimerRef.current !== null) { + window.clearTimeout(adminPromptTimerRef.current); + adminPromptTimerRef.current = null; + } + }; + }, [hasAdminPrompt]); + useEffect(() => { return () => { if (serviceVisualTimerRef.current !== null) { window.clearTimeout(serviceVisualTimerRef.current); } + if (adminPromptTimerRef.current !== null) { + window.clearTimeout(adminPromptTimerRef.current); + } }; }, []); @@ -1017,29 +1043,64 @@ export function App() { }, 700); } + function toggleAdminPrompt() { + if (adminPromptTimerRef.current !== null) { + window.clearTimeout(adminPromptTimerRef.current); + adminPromptTimerRef.current = null; + } + setIsAdminPromptHintVisible(false); + setIsAdminPromptOpen((open) => !open); + } + function renderAdminPrompt() { if (!hasAdminPrompt) return null; return ( - ); } @@ -1690,6 +1751,11 @@ export function App() { align={isVertical || index >= segments.length - 2 ? "end" : "start"} aria-label={`${segment.label}: ${segment.value}. ${segment.details.join(". ")}`} key={segment.id} + style={ + isVertical + ? ({ animationDelay: `${index * 280}ms` } as CSSProperties) + : undefined + } >