mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 23:56:39 +00:00
fix: make Reduced motion gate overlays, standardize the tab band, and polish fleet/snapshots (#1504)
* fix: address review feedback on motion, fleet band, and snapshots - Reduced motion now also skips the animate-ui overlay open/close animations (dialog, sheet, popover, dropdown-menu) via a shared useReducedTransition that zeroes the transition when reduced motion is active; MotionConfig alone only neutralized transform/layout, leaving the opacity/blur fade. - Fleet tab band: flatten the list's own pill band so the tabs sit in a single full-width band instead of a nested second band. - Fleet Overview: label the node-update button "Node Update"; the add-node button becomes a ChartNetwork icon that opens Settings > Nodes (outline style, matching the node-update button) instead of the add-node modal. - Blueprint deployments empty state: drive the serif headings from the theme heading-style token so Calm drops the italic. - Snapshots: move the per-stack Restore onto the stack header row (right side), matching the ghost action style. * feat: standardize the tab band on Security and add a tab hover state - Apply the Fleet full-width tab band to the Security page (single band, the list's pill background flattened so the tabs sit directly in it). - Add a hover highlight to tabs so the band reads as interactive, not flat; the active tab is unaffected. * fix: drive reduced-motion transitions from the app setting, not the OS query useReducedTransition called framer-motion's useReducedMotion(), which only reflects the OS prefers-reduced-motion media query and ignores our MotionConfig / appearance toggle, so dialogs kept animating with the setting on. Read the app's useReducedMotion selector instead. Verified: with the setting on, the dialog opacity snaps 0->1 in one frame; with it off, it ramps over the spring. * feat: standardize stack-detail body text to text-xs and add a log expand toggle - Bump the anatomy panel body text (rows, field values, warnings) and the structured log lines from 10/11/12px to text-xs (12px), leaving the uppercase kicker labels and the log level/source badges as-is. - Add an expand control next to the log download button that collapses the Command Center so the logs pane fills the left column; toggles back to restore.
This commit is contained in:
@@ -7,6 +7,7 @@ import { AnimatePresence, motion, type HTMLMotionProps } from 'motion/react';
|
||||
|
||||
import { useControlledState } from '@/hooks/use-controlled-state';
|
||||
import { getStrictContext } from '@/lib/get-strict-context';
|
||||
import { useReducedTransition } from '@/components/animate-ui/primitives/use-reduced-transition';
|
||||
|
||||
type DialogContextType = {
|
||||
isOpen: boolean;
|
||||
@@ -73,6 +74,7 @@ function DialogOverlay({
|
||||
transition = { duration: 0.2, ease: 'easeInOut' },
|
||||
...props
|
||||
}: DialogOverlayProps) {
|
||||
const resolvedTransition = useReducedTransition(transition);
|
||||
return (
|
||||
<DialogPrimitive.Overlay data-slot="dialog-overlay" asChild forceMount>
|
||||
<motion.div
|
||||
@@ -80,7 +82,7 @@ function DialogOverlay({
|
||||
initial={{ opacity: 0, filter: 'blur(4px)' }}
|
||||
animate={{ opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ opacity: 0, filter: 'blur(4px)' }}
|
||||
transition={transition}
|
||||
transition={resolvedTransition}
|
||||
{...props}
|
||||
/>
|
||||
</DialogPrimitive.Overlay>
|
||||
@@ -111,6 +113,7 @@ function DialogContent({
|
||||
from === 'bottom' || from === 'left' ? '20deg' : '-20deg';
|
||||
const isVertical = from === 'top' || from === 'bottom';
|
||||
const rotateAxis = isVertical ? 'rotateX' : 'rotateY';
|
||||
const resolvedTransition = useReducedTransition(transition);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Content
|
||||
@@ -140,7 +143,7 @@ function DialogContent({
|
||||
filter: 'blur(4px)',
|
||||
transform: `perspective(500px) ${rotateAxis}(${initialRotation}) scale(0.8)`,
|
||||
}}
|
||||
transition={transition}
|
||||
transition={resolvedTransition}
|
||||
{...props}
|
||||
/>
|
||||
</DialogPrimitive.Content>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@/components/animate-ui/primitives/effects/highlight';
|
||||
import { getStrictContext } from '@/lib/get-strict-context';
|
||||
import { useControlledState } from '@/hooks/use-controlled-state';
|
||||
import { useReducedTransition } from '@/components/animate-ui/primitives/use-reduced-transition';
|
||||
import { useDataState } from '@/hooks/use-data-state';
|
||||
|
||||
type DropdownMenuContextType = {
|
||||
@@ -198,6 +199,7 @@ function DropdownMenuSubContent({
|
||||
...props
|
||||
}: DropdownMenuSubContentProps) {
|
||||
const { isOpen } = useDropdownMenuSub();
|
||||
const resolvedTransition = useReducedTransition(transition);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@@ -226,7 +228,7 @@ function DropdownMenuSubContent({
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={transition}
|
||||
transition={resolvedTransition}
|
||||
style={{ willChange: 'opacity, transform', ...style }}
|
||||
{...props}
|
||||
/>
|
||||
@@ -295,6 +297,7 @@ function DropdownMenuContent({
|
||||
...props
|
||||
}: DropdownMenuContentProps) {
|
||||
const { isOpen } = useDropdownMenu();
|
||||
const resolvedTransition = useReducedTransition(transition);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@@ -325,7 +328,7 @@ function DropdownMenuContent({
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={transition}
|
||||
transition={resolvedTransition}
|
||||
style={{ willChange: 'opacity, transform', ...style }}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AnimatePresence, motion, type HTMLMotionProps } from 'motion/react';
|
||||
|
||||
import { getStrictContext } from '@/lib/get-strict-context';
|
||||
import { useControlledState } from '@/hooks/use-controlled-state';
|
||||
import { useReducedTransition } from '@/components/animate-ui/primitives/use-reduced-transition';
|
||||
|
||||
type PopoverContextType = {
|
||||
isOpen: boolean;
|
||||
@@ -91,6 +92,7 @@ function PopoverContent({
|
||||
transition = { type: 'spring', stiffness: 300, damping: 25 },
|
||||
...props
|
||||
}: PopoverContentProps) {
|
||||
const resolvedTransition = useReducedTransition(transition);
|
||||
return (
|
||||
<PopoverPrimitive.Content
|
||||
asChild
|
||||
@@ -118,7 +120,7 @@ function PopoverContent({
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.5 }}
|
||||
transition={transition}
|
||||
transition={resolvedTransition}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Content>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AnimatePresence, motion, type HTMLMotionProps } from 'motion/react';
|
||||
|
||||
import { getStrictContext } from '@/lib/get-strict-context';
|
||||
import { useControlledState } from '@/hooks/use-controlled-state';
|
||||
import { useReducedTransition } from '@/components/animate-ui/primitives/use-reduced-transition';
|
||||
|
||||
type SheetContextType = {
|
||||
isOpen: boolean;
|
||||
@@ -72,6 +73,7 @@ function SheetOverlay({
|
||||
transition = { duration: 0.2, ease: 'easeInOut' },
|
||||
...props
|
||||
}: SheetOverlayProps) {
|
||||
const resolvedTransition = useReducedTransition(transition);
|
||||
return (
|
||||
<SheetPrimitive.Overlay asChild forceMount>
|
||||
<motion.div
|
||||
@@ -80,7 +82,7 @@ function SheetOverlay({
|
||||
initial={{ opacity: 0, filter: 'blur(4px)' }}
|
||||
animate={{ opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ opacity: 0, filter: 'blur(4px)' }}
|
||||
transition={transition}
|
||||
transition={resolvedTransition}
|
||||
{...props}
|
||||
/>
|
||||
</SheetPrimitive.Overlay>
|
||||
@@ -102,6 +104,7 @@ function SheetContent({
|
||||
...props
|
||||
}: SheetContentProps) {
|
||||
const axis = side === 'left' || side === 'right' ? 'x' : 'y';
|
||||
const resolvedTransition = useReducedTransition(transition);
|
||||
|
||||
const offscreen: Record<Side, { x?: string; y?: string; opacity: number }> = {
|
||||
right: { x: '100%', opacity: 0 },
|
||||
@@ -131,7 +134,7 @@ function SheetContent({
|
||||
...positionStyle[side],
|
||||
...style,
|
||||
}}
|
||||
transition={transition}
|
||||
transition={resolvedTransition}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useReducedMotion } from '@/hooks/use-theme';
|
||||
|
||||
const INSTANT = { duration: 0 } as const;
|
||||
|
||||
/**
|
||||
* Returns an instant (zero-duration) transition when the "Reduced motion"
|
||||
* appearance setting is on, otherwise the supplied transition. This reads our
|
||||
* app setting (not framer-motion's useReducedMotion, which only reflects the OS
|
||||
* prefers-reduced-motion). `MotionConfig`'s reducedMotion only neutralizes
|
||||
* transform/layout animations, so overlay primitives that fade or blur
|
||||
* (opacity/filter) still animate; this collapses those too.
|
||||
*/
|
||||
export function useReducedTransition<T>(transition: T): T | typeof INSTANT {
|
||||
return useReducedMotion() ? INSTANT : transition;
|
||||
}
|
||||
Reference in New Issue
Block a user