@wallarm-org/design-system 0.54.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,15 @@
1
1
  import { type FC, type HTMLAttributes, type ReactNode, type Ref } from 'react';
2
2
  import { type TestableProps } from '../../utils/testId';
3
+ export interface AppShellExpandFrom {
4
+ width: number;
5
+ height: number;
6
+ borderRadius?: number;
7
+ }
3
8
  export interface AppShellProps extends HTMLAttributes<HTMLDivElement>, TestableProps {
4
9
  ref?: Ref<HTMLDivElement>;
5
10
  children?: ReactNode;
6
11
  reveal?: boolean;
12
+ expandFrom?: AppShellExpandFrom;
7
13
  onRevealed?: () => void;
8
14
  appeared?: boolean;
9
15
  }
@@ -1,51 +1,20 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { useMemo, useRef } from "react";
3
3
  import { composeRefs } from "@radix-ui/react-compose-refs";
4
4
  import { cn } from "../../utils/cn.js";
5
5
  import { TestIdProvider } from "../../utils/testId.js";
6
6
  import { AppShellContext } from "./AppShellContext.js";
7
- const AppShell = ({ ref, className, children, reveal, onRevealed, appeared: appearedProp, 'data-testid': testId, ...props })=>{
7
+ import { ExpandOverlay } from "./ExpandOverlay.js";
8
+ import { RevealOverlay } from "./RevealOverlay.js";
9
+ import { useShellAnimation } from "./useShellAnimation.js";
10
+ const AppShell = ({ ref, className, children, reveal, expandFrom, onRevealed, appeared: appearedProp, 'data-testid': testId, ...props })=>{
8
11
  const internalRef = useRef(null);
9
- const onRevealedRef = useRef(onRevealed);
10
- onRevealedRef.current = onRevealed;
11
- const [phase, setPhase] = useState(reveal ? 'initial' : 'done');
12
- const [targetRect, setTargetRect] = useState(null);
13
- useEffect(()=>{
14
- if ('initial' !== phase) return;
15
- const shell = internalRef.current;
16
- if (!shell) return;
17
- let cancelled = false;
18
- requestAnimationFrame(()=>{
19
- requestAnimationFrame(()=>{
20
- if (cancelled) return;
21
- const remote = shell.querySelector('[data-slot="app-shell-remote"]');
22
- if (!remote) {
23
- setPhase('done');
24
- onRevealedRef.current?.();
25
- return;
26
- }
27
- const shellRect = shell.getBoundingClientRect();
28
- const remoteRect = remote.getBoundingClientRect();
29
- setTargetRect({
30
- top: remoteRect.top - shellRect.top,
31
- left: remoteRect.left - shellRect.left
32
- });
33
- setPhase('revealing');
34
- });
35
- });
36
- return ()=>{
37
- cancelled = true;
38
- };
39
- }, [
40
- phase
41
- ]);
42
- const handleTransitionEnd = useCallback((e)=>{
43
- if ('top' === e.propertyName) {
44
- setPhase('done');
45
- onRevealedRef.current?.();
46
- }
47
- }, []);
48
- const appeared = appearedProp ?? 'done' === phase;
12
+ const { phase, targetRect, expandStyle, appeared, handleRevealOverlayTransitionEnd, handleExpandTransitionEnd } = useShellAnimation(internalRef, {
13
+ reveal,
14
+ expandFrom,
15
+ onRevealed,
16
+ appearedProp
17
+ });
49
18
  const shellContext = useMemo(()=>({
50
19
  appeared
51
20
  }), [
@@ -61,16 +30,19 @@ const AppShell = ({ ref, className, children, reveal, onRevealed, appeared: appe
61
30
  "data-slot": "app-shell",
62
31
  "data-testid": testId,
63
32
  className: cn('relative grid h-screen overscroll-none [grid-template-areas:"header_header""rail_remote"] [grid-template-columns:auto_1fr] [grid-template-rows:auto_1fr] bg-component-app-shell-bg', className),
33
+ style: expandStyle,
34
+ onTransitionEnd: expandFrom ? handleExpandTransitionEnd : void 0,
64
35
  children: [
65
36
  children,
66
- 'done' !== phase && /*#__PURE__*/ jsx("div", {
67
- className: cn('bg-bg-page-bg pointer-events-none z-50 absolute right-0 bottom-0', 'revealing' === phase && 'rounded-tl-12'),
68
- style: {
69
- top: 'revealing' === phase && targetRect ? targetRect.top : 0,
70
- left: 'revealing' === phase && targetRect ? targetRect.left : 0,
71
- transition: 'revealing' === phase ? 'top 500ms ease-in-out, left 500ms ease-in-out, border-radius 500ms ease-in-out' : void 0
72
- },
73
- onTransitionEnd: handleTransitionEnd
37
+ expandFrom && /*#__PURE__*/ jsx(ExpandOverlay, {
38
+ phase: phase,
39
+ expandFrom: expandFrom,
40
+ targetRect: targetRect
41
+ }),
42
+ !expandFrom && /*#__PURE__*/ jsx(RevealOverlay, {
43
+ phase: phase,
44
+ targetRect: targetRect,
45
+ onTransitionEnd: handleRevealOverlayTransitionEnd
74
46
  })
75
47
  ]
76
48
  })
@@ -10,7 +10,7 @@ const AppShellHeader = ({ ref, className, children, ...props })=>{
10
10
  ref: ref,
11
11
  "data-slot": "app-shell-header",
12
12
  "data-testid": testId,
13
- className: cn('[grid-area:header] transition-opacity duration-200 ease-in-out', !appeared && 'opacity-0', className),
13
+ className: cn('[grid-area:header] transition-opacity duration-500 ease-in-out', !appeared && 'opacity-0', className),
14
14
  children: children
15
15
  });
16
16
  };
@@ -10,7 +10,7 @@ const AppShellRail = ({ ref, className, children, ...props })=>{
10
10
  ref: ref,
11
11
  "data-slot": "app-shell-rail",
12
12
  "data-testid": testId,
13
- className: cn('[grid-area:rail] transition-opacity duration-200 ease-in-out', !appeared && 'opacity-0', className),
13
+ className: cn('[grid-area:rail] transition-opacity duration-500 ease-in-out', !appeared && 'opacity-0', className),
14
14
  children: children
15
15
  });
16
16
  };
@@ -0,0 +1,13 @@
1
+ import type { FC } from 'react';
2
+ import type { AppShellExpandFrom } from './AppShell';
3
+ import type { RevealPhase } from './useShellAnimation';
4
+ interface ExpandOverlayProps {
5
+ phase: RevealPhase;
6
+ expandFrom: AppShellExpandFrom;
7
+ targetRect: {
8
+ top: number;
9
+ left: number;
10
+ } | null;
11
+ }
12
+ export declare const ExpandOverlay: FC<ExpandOverlayProps>;
13
+ export {};
@@ -0,0 +1,36 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { EXPAND_BORDER_MS, HERO_EASE } from "./constants.js";
3
+ const EXPANDING_TRANSITION = [
4
+ 'top',
5
+ 'left',
6
+ 'right',
7
+ 'bottom',
8
+ 'border-radius',
9
+ 'border-right-width',
10
+ 'border-bottom-width'
11
+ ].map((prop)=>`${prop} ${EXPAND_BORDER_MS}ms ${HERO_EASE}`).join(', ');
12
+ const ExpandOverlay = ({ phase, expandFrom, targetRect })=>{
13
+ if ('initial' !== phase && 'expanding' !== phase) return null;
14
+ const { width, height, borderRadius = 0 } = expandFrom;
15
+ const isInitial = 'initial' === phase;
16
+ const halfW = `calc(50% - ${width / 2}px)`;
17
+ const halfH = `calc(50% - ${height / 2}px)`;
18
+ return /*#__PURE__*/ jsx("div", {
19
+ className: "pointer-events-none absolute z-50 border-solid border-border-primary-light",
20
+ style: {
21
+ top: isInitial ? halfH : `${targetRect?.top ?? 0}px`,
22
+ left: isInitial ? halfW : `${targetRect?.left ?? 0}px`,
23
+ right: isInitial ? halfW : 0,
24
+ bottom: isInitial ? halfH : 0,
25
+ borderTopWidth: 1,
26
+ borderLeftWidth: 1,
27
+ borderRightWidth: isInitial ? 1 : 0,
28
+ borderBottomWidth: isInitial ? 1 : 0,
29
+ borderRadius: isInitial ? borderRadius : '12px 0px 0px 0px',
30
+ opacity: 1,
31
+ transition: isInitial ? void 0 : EXPANDING_TRANSITION
32
+ }
33
+ });
34
+ };
35
+ ExpandOverlay.displayName = 'ExpandOverlay';
36
+ export { ExpandOverlay };
@@ -0,0 +1,12 @@
1
+ import type { FC } from 'react';
2
+ import type { RevealPhase } from './useShellAnimation';
3
+ interface RevealOverlayProps {
4
+ phase: RevealPhase;
5
+ targetRect: {
6
+ top: number;
7
+ left: number;
8
+ } | null;
9
+ onTransitionEnd: (e: React.TransitionEvent) => void;
10
+ }
11
+ export declare const RevealOverlay: FC<RevealOverlayProps>;
12
+ export {};
@@ -0,0 +1,23 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cn } from "../../utils/cn.js";
3
+ import { EMPHASIZED_DECEL, MORPH_MS } from "./constants.js";
4
+ const REVEALING_TRANSITION = [
5
+ 'top',
6
+ 'left',
7
+ 'border-radius'
8
+ ].map((prop)=>`${prop} ${MORPH_MS}ms ${EMPHASIZED_DECEL}`).join(', ');
9
+ const RevealOverlay = ({ phase, targetRect, onTransitionEnd })=>{
10
+ if ('done' === phase) return null;
11
+ const isRevealing = 'revealing' === phase;
12
+ return /*#__PURE__*/ jsx("div", {
13
+ className: cn('bg-bg-page-bg pointer-events-none z-50 absolute right-0 bottom-0', isRevealing && 'rounded-tl-12'),
14
+ style: {
15
+ top: isRevealing && targetRect ? targetRect.top : 0,
16
+ left: isRevealing && targetRect ? targetRect.left : 0,
17
+ transition: isRevealing ? REVEALING_TRANSITION : void 0
18
+ },
19
+ onTransitionEnd: onTransitionEnd
20
+ });
21
+ };
22
+ RevealOverlay.displayName = 'RevealOverlay';
23
+ export { RevealOverlay };
@@ -0,0 +1,6 @@
1
+ export declare const EMPHASIZED_DECEL = "cubic-bezier(0.05, 0.7, 0.1, 1)";
2
+ export declare const HERO_EASE = "cubic-bezier(0.4, 0, 0.2, 1)";
3
+ export declare const MORPH_MS = 400;
4
+ export declare const EXPAND_MS = 560;
5
+ export declare const EXPAND_BORDER_MS = 400;
6
+ export declare const SKELETON_DELAY = 250;
@@ -0,0 +1,7 @@
1
+ const EMPHASIZED_DECEL = 'cubic-bezier(0.05, 0.7, 0.1, 1)';
2
+ const HERO_EASE = 'cubic-bezier(0.4, 0, 0.2, 1)';
3
+ const MORPH_MS = 400;
4
+ const EXPAND_MS = 560;
5
+ const EXPAND_BORDER_MS = 400;
6
+ const SKELETON_DELAY = 250;
7
+ export { EMPHASIZED_DECEL, EXPAND_BORDER_MS, EXPAND_MS, HERO_EASE, MORPH_MS, SKELETON_DELAY };
@@ -1,4 +1,4 @@
1
- export { AppShell, type AppShellProps } from './AppShell';
1
+ export { AppShell, type AppShellExpandFrom, type AppShellProps } from './AppShell';
2
2
  export { AppShellHeader, type AppShellHeaderProps } from './AppShellHeader';
3
3
  export { AppShellRail, type AppShellRailProps } from './AppShellRail';
4
4
  export { AppShellRemote, type AppShellRemoteProps } from './AppShellRemote';
@@ -0,0 +1 @@
1
+ export declare const HeaderActions: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,75 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { Bell, ChevronUpDown } from "../../../icons/index.js";
3
+ import { Button } from "../../Button/index.js";
4
+ import { Code } from "../../Code/index.js";
5
+ import { Kbd } from "../../Kbd/index.js";
6
+ import { Text } from "../../Text/index.js";
7
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../../Tooltip/index.js";
8
+ import { TopHeaderSeparator } from "../../TopHeader/index.js";
9
+ import { QuickHelpDropdown } from "./_storyQuickHelpDropdown.js";
10
+ const HeaderActions = ()=>/*#__PURE__*/ jsxs(Fragment, {
11
+ children: [
12
+ /*#__PURE__*/ jsxs(Button, {
13
+ variant: "ghost",
14
+ size: "small",
15
+ color: "neutral",
16
+ className: "p-4 gap-6 rounded-6",
17
+ children: [
18
+ /*#__PURE__*/ jsx(Code, {
19
+ size: "s",
20
+ color: "secondary",
21
+ children: "Search Wallarm"
22
+ }),
23
+ /*#__PURE__*/ jsx(Kbd, {
24
+ size: "xsmall",
25
+ children: "⌘ K"
26
+ })
27
+ ]
28
+ }),
29
+ /*#__PURE__*/ jsx(TopHeaderSeparator, {}),
30
+ /*#__PURE__*/ jsxs(Button, {
31
+ variant: "ghost",
32
+ size: "small",
33
+ color: "neutral",
34
+ className: "py-4 rounded-6",
35
+ children: [
36
+ /*#__PURE__*/ jsx(Text, {
37
+ size: "xs",
38
+ weight: "medium",
39
+ children: "Tenant Name"
40
+ }),
41
+ /*#__PURE__*/ jsx("span", {
42
+ className: "text-text-tertiary mx-[-2px]",
43
+ children: "•"
44
+ }),
45
+ /*#__PURE__*/ jsx(Code, {
46
+ size: "s",
47
+ color: "secondary",
48
+ children: "12345"
49
+ }),
50
+ /*#__PURE__*/ jsx(ChevronUpDown, {
51
+ className: "!icon-sm"
52
+ })
53
+ ]
54
+ }),
55
+ /*#__PURE__*/ jsxs(Tooltip, {
56
+ children: [
57
+ /*#__PURE__*/ jsx(TooltipTrigger, {
58
+ asChild: true,
59
+ children: /*#__PURE__*/ jsx(Button, {
60
+ variant: "ghost",
61
+ size: "small",
62
+ color: "neutral",
63
+ "aria-label": "Wallarm Updates",
64
+ children: /*#__PURE__*/ jsx(Bell, {})
65
+ })
66
+ }),
67
+ /*#__PURE__*/ jsx(TooltipContent, {
68
+ children: "Wallarm updates"
69
+ })
70
+ ]
71
+ }),
72
+ /*#__PURE__*/ jsx(QuickHelpDropdown, {})
73
+ ]
74
+ });
75
+ export { HeaderActions };
@@ -0,0 +1,13 @@
1
+ import type { FC } from 'react';
2
+ import type { Theme } from '../../ThemeProvider';
3
+ import { type SidebarMode } from './_storyAccountDropdown';
4
+ import { type Product } from './_storyLib';
5
+ interface NavRailFooterContentProps {
6
+ activeProduct: Product;
7
+ sidebarMode: SidebarMode;
8
+ onSidebarModeChange: (mode: SidebarMode) => void;
9
+ theme: Theme;
10
+ onThemeChange: (theme: Theme) => void;
11
+ }
12
+ export declare const NavRailFooterContent: FC<NavRailFooterContentProps>;
13
+ export {};
@@ -0,0 +1,26 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Settings } from "../../../icons/index.js";
3
+ import { NavRailFooter, NavRailItem } from "../../NavRail/index.js";
4
+ import { AccountDropdown } from "./_storyAccountDropdown.js";
5
+ import { navigateToProduct } from "./_storyLib.js";
6
+ const NavRailFooterContent = ({ activeProduct, sidebarMode, onSidebarModeChange, theme, onThemeChange })=>/*#__PURE__*/ jsxs(NavRailFooter, {
7
+ children: [
8
+ /*#__PURE__*/ jsx(NavRailItem, {
9
+ icon: Settings,
10
+ label: "Settings",
11
+ shortcut: [
12
+ 'G',
13
+ 'S'
14
+ ],
15
+ active: 'settings' === activeProduct,
16
+ onClick: ()=>navigateToProduct('settings')
17
+ }),
18
+ /*#__PURE__*/ jsx(AccountDropdown, {
19
+ sidebarMode: sidebarMode,
20
+ onSidebarModeChange: onSidebarModeChange,
21
+ theme: theme,
22
+ onThemeChange: onThemeChange
23
+ })
24
+ ]
25
+ });
26
+ export { NavRailFooterContent };
@@ -0,0 +1,7 @@
1
+ import type { FC } from 'react';
2
+ import { type Product } from './_storyLib';
3
+ interface ProductNavItemsProps {
4
+ activeProduct: Product;
5
+ }
6
+ export declare const ProductNavItems: FC<ProductNavItemsProps>;
7
+ export {};
@@ -0,0 +1,49 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { CircleDashed } from "../../../icons/index.js";
3
+ import { NavRailItem } from "../../NavRail/index.js";
4
+ import { navigateToProduct } from "./_storyLib.js";
5
+ const ProductNavItems = ({ activeProduct })=>/*#__PURE__*/ jsxs(Fragment, {
6
+ children: [
7
+ /*#__PURE__*/ jsx(NavRailItem, {
8
+ icon: CircleDashed,
9
+ label: "Edge",
10
+ shortcut: [
11
+ 'G',
12
+ 'E'
13
+ ],
14
+ active: 'edge' === activeProduct,
15
+ onClick: ()=>navigateToProduct('edge')
16
+ }),
17
+ /*#__PURE__*/ jsx(NavRailItem, {
18
+ icon: CircleDashed,
19
+ label: "AI Hypervisor",
20
+ shortcut: [
21
+ 'G',
22
+ 'A'
23
+ ],
24
+ active: 'ai-hypervisor' === activeProduct,
25
+ onClick: ()=>navigateToProduct('ai-hypervisor')
26
+ }),
27
+ /*#__PURE__*/ jsx(NavRailItem, {
28
+ icon: CircleDashed,
29
+ label: "Infra Discovery",
30
+ shortcut: [
31
+ 'G',
32
+ 'I'
33
+ ],
34
+ active: 'infra-discovery' === activeProduct,
35
+ onClick: ()=>navigateToProduct('infra-discovery')
36
+ }),
37
+ /*#__PURE__*/ jsx(NavRailItem, {
38
+ icon: CircleDashed,
39
+ label: "Security Testing",
40
+ shortcut: [
41
+ 'G',
42
+ 'T'
43
+ ],
44
+ active: 'security-testing' === activeProduct,
45
+ onClick: ()=>navigateToProduct('security-testing')
46
+ })
47
+ ]
48
+ });
49
+ export { ProductNavItems };
@@ -1,8 +1,11 @@
1
1
  export { AccountDropdown, type SidebarMode } from './_storyAccountDropdown';
2
2
  export { RemoteForProduct } from './_storyConfigRenderer';
3
+ export { HeaderActions } from './_storyHeaderActions';
3
4
  export { HomeContent } from './_storyHomeContent';
4
5
  export { deriveProduct, navigateToProduct } from './_storyLib';
5
6
  export { aiHypervisorNavConfig, edgeNavConfig, infraDiscoveryNavConfig, securityTestingNavConfig, settingsNavConfig, } from './_storyNavConfigs';
7
+ export { NavRailFooterContent } from './_storyNavRailFooter';
8
+ export { ProductNavItems } from './_storyProductNavItems';
6
9
  export { QuickHelpDropdown } from './_storyQuickHelpDropdown';
7
10
  export { RecentDropdown } from './_storyRecentDropdown';
8
11
  export { WallarmLogo } from './_storyWallarmLogo';
@@ -1,9 +1,12 @@
1
1
  import { AccountDropdown } from "./_storyAccountDropdown.js";
2
2
  import { RemoteForProduct } from "./_storyConfigRenderer.js";
3
+ import { HeaderActions } from "./_storyHeaderActions.js";
3
4
  import { HomeContent } from "./_storyHomeContent.js";
4
5
  import { deriveProduct, navigateToProduct } from "./_storyLib.js";
5
6
  import { aiHypervisorNavConfig, edgeNavConfig, infraDiscoveryNavConfig, securityTestingNavConfig, settingsNavConfig } from "./_storyNavConfigs.js";
7
+ import { NavRailFooterContent } from "./_storyNavRailFooter.js";
8
+ import { ProductNavItems } from "./_storyProductNavItems.js";
6
9
  import { QuickHelpDropdown } from "./_storyQuickHelpDropdown.js";
7
10
  import { RecentDropdown } from "./_storyRecentDropdown.js";
8
11
  import { WallarmLogo } from "./_storyWallarmLogo.js";
9
- export { AccountDropdown, HomeContent, QuickHelpDropdown, RecentDropdown, RemoteForProduct, WallarmLogo, aiHypervisorNavConfig, deriveProduct, edgeNavConfig, infraDiscoveryNavConfig, navigateToProduct, securityTestingNavConfig, settingsNavConfig };
12
+ export { AccountDropdown, HeaderActions, HomeContent, NavRailFooterContent, ProductNavItems, QuickHelpDropdown, RecentDropdown, RemoteForProduct, WallarmLogo, aiHypervisorNavConfig, deriveProduct, edgeNavConfig, infraDiscoveryNavConfig, navigateToProduct, securityTestingNavConfig, settingsNavConfig };
@@ -0,0 +1,21 @@
1
+ import { type RefObject } from 'react';
2
+ import type { AppShellExpandFrom } from './AppShell';
3
+ export type RevealPhase = 'initial' | 'expanding' | 'revealing' | 'done';
4
+ interface UseShellAnimationOptions {
5
+ reveal?: boolean;
6
+ expandFrom?: AppShellExpandFrom;
7
+ onRevealed?: () => void;
8
+ appearedProp?: boolean;
9
+ }
10
+ export declare function useShellAnimation(shellRef: RefObject<HTMLDivElement | null>, options: UseShellAnimationOptions): {
11
+ phase: RevealPhase;
12
+ targetRect: {
13
+ top: number;
14
+ left: number;
15
+ } | null;
16
+ expandStyle: import("react").CSSProperties | undefined;
17
+ appeared: boolean;
18
+ handleRevealOverlayTransitionEnd: (e: React.TransitionEvent) => void;
19
+ handleExpandTransitionEnd: (e: React.TransitionEvent) => void;
20
+ };
21
+ export {};
@@ -0,0 +1,116 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { EXPAND_BORDER_MS, EXPAND_MS, HERO_EASE, SKELETON_DELAY } from "./constants.js";
3
+ function queryRemoteRect(shell) {
4
+ const remote = shell.querySelector('[data-slot="app-shell-remote"]');
5
+ if (!remote) return null;
6
+ const shellRect = shell.getBoundingClientRect();
7
+ const remoteRect = remote.getBoundingClientRect();
8
+ return {
9
+ top: remoteRect.top - shellRect.top,
10
+ left: remoteRect.left - shellRect.left
11
+ };
12
+ }
13
+ function computeExpandStyle(expandFrom, phase, targetRect) {
14
+ const { width, height, borderRadius = 0 } = expandFrom;
15
+ switch(phase){
16
+ case 'initial':
17
+ return {
18
+ clipPath: `inset(calc(50% - ${height / 2}px) calc(50% - ${width / 2}px) round ${borderRadius}px)`
19
+ };
20
+ case 'expanding':
21
+ if (targetRect) return {
22
+ clipPath: `inset(${targetRect.top}px 0px 0px ${targetRect.left}px round 12px 0px 0px 0px)`,
23
+ transition: `clip-path ${EXPAND_BORDER_MS}ms ${HERO_EASE}`
24
+ };
25
+ return {
26
+ clipPath: 'inset(0 0 round 0px)',
27
+ transition: `clip-path ${EXPAND_BORDER_MS}ms ${HERO_EASE}`
28
+ };
29
+ default:
30
+ return;
31
+ }
32
+ }
33
+ function useShellAnimation(shellRef, options) {
34
+ const { reveal, expandFrom, onRevealed, appearedProp } = options;
35
+ const onRevealedRef = useRef(onRevealed);
36
+ onRevealedRef.current = onRevealed;
37
+ const hasAnimation = Boolean(expandFrom || reveal);
38
+ const [phase, setPhase] = useState(hasAnimation ? 'initial' : 'done');
39
+ const [targetRect, setTargetRect] = useState(null);
40
+ useEffect(()=>{
41
+ if ('initial' !== phase) return;
42
+ let cancelled = false;
43
+ const scheduleDoubleRaf = (callback)=>{
44
+ requestAnimationFrame(()=>{
45
+ requestAnimationFrame(()=>{
46
+ if (!cancelled) callback();
47
+ });
48
+ });
49
+ };
50
+ scheduleDoubleRaf(expandFrom ? ()=>{
51
+ const shell = shellRef.current;
52
+ if (shell) {
53
+ const rect = queryRemoteRect(shell);
54
+ if (rect) setTargetRect(rect);
55
+ }
56
+ setPhase('expanding');
57
+ } : ()=>{
58
+ const shell = shellRef.current;
59
+ if (!shell) return;
60
+ const rect = queryRemoteRect(shell);
61
+ if (!rect) {
62
+ setPhase('done');
63
+ onRevealedRef.current?.();
64
+ return;
65
+ }
66
+ setTargetRect(rect);
67
+ setPhase('revealing');
68
+ });
69
+ return ()=>{
70
+ cancelled = true;
71
+ };
72
+ }, [
73
+ phase,
74
+ expandFrom,
75
+ shellRef
76
+ ]);
77
+ const handleRevealOverlayTransitionEnd = useCallback((e)=>{
78
+ if ('top' === e.propertyName) {
79
+ setPhase('done');
80
+ onRevealedRef.current?.();
81
+ }
82
+ }, []);
83
+ const handleExpandTransitionEnd = useCallback((e)=>{
84
+ if ('clip-path' === e.propertyName) {
85
+ setPhase('done');
86
+ onRevealedRef.current?.();
87
+ }
88
+ }, []);
89
+ const expandStyle = useMemo(()=>expandFrom ? computeExpandStyle(expandFrom, phase, targetRect) : void 0, [
90
+ expandFrom,
91
+ phase,
92
+ targetRect
93
+ ]);
94
+ const isDone = 'done' === phase;
95
+ const [appearedDelayed, setAppearedDelayed] = useState(!hasAnimation);
96
+ useEffect(()=>{
97
+ if (!isDone || !hasAnimation) return;
98
+ const delay = expandFrom ? EXPAND_MS - EXPAND_BORDER_MS : SKELETON_DELAY;
99
+ const id = setTimeout(()=>setAppearedDelayed(true), delay);
100
+ return ()=>clearTimeout(id);
101
+ }, [
102
+ isDone,
103
+ hasAnimation,
104
+ expandFrom
105
+ ]);
106
+ const appeared = appearedProp ?? (hasAnimation ? appearedDelayed : true);
107
+ return {
108
+ phase,
109
+ targetRect,
110
+ expandStyle,
111
+ appeared,
112
+ handleRevealOverlayTransitionEnd,
113
+ handleExpandTransitionEnd
114
+ };
115
+ }
116
+ export { useShellAnimation };
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.53.1",
3
- "generatedAt": "2026-06-04T00:25:40.326Z",
2
+ "version": "0.54.0",
3
+ "generatedAt": "2026-06-04T22:49:52.517Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
@@ -3724,6 +3724,11 @@
3724
3724
  "type": "boolean | undefined",
3725
3725
  "required": false
3726
3726
  },
3727
+ {
3728
+ "name": "expandFrom",
3729
+ "type": "AppShellExpandFrom | undefined",
3730
+ "required": false
3731
+ },
3727
3732
  {
3728
3733
  "name": "appeared",
3729
3734
  "type": "boolean | undefined",
@@ -4831,7 +4836,15 @@
4831
4836
  "examples": [
4832
4837
  {
4833
4838
  "name": "Basic",
4834
- "code": "() => {\n const pathname = useLocationPathname();\n const activeProduct = deriveProduct(pathname);\n\n const [loading, setLoading] = useState(true);\n const [sidebarMode, setSidebarMode] = useState<SidebarMode>('adaptive');\n const [revealKey, setRevealKey] = useState(0);\n const { theme, setTheme } = useTheme();\n const collapsed = sidebarMode === 'adaptive' && activeProduct !== 'home';\n\n return (\n <AppShell key={revealKey} reveal>\n <AppShellHeader>\n <TopHeader>\n <TopHeaderLogo href='/'>\n <WallarmLogo />\n </TopHeaderLogo>\n\n <TopHeaderActions>\n {loading ? (\n <>\n <Skeleton width='150px' height='20px' rounded={6} />\n <TopHeaderSeparator />\n <Skeleton width='150px' height='20px' rounded={6} />\n </>\n ) : (\n <>\n <Button\n variant='ghost'\n size='small'\n color='neutral'\n className='p-4 gap-6 rounded-6'\n >\n <Code size='s' color='secondary'>\n Search Wallarm\n </Code>\n <Kbd size='xsmall'>⌘ K</Kbd>\n </Button>\n\n <TopHeaderSeparator />\n\n <Button variant='ghost' size='small' color='neutral' className='py-4 rounded-6'>\n <Text size='xs' weight='medium'>\n Tenant Name\n </Text>\n <span className='text-text-tertiary mx-[-2px]'>•</span>\n <Code size='s' color='secondary'>\n 12345\n </Code>\n <ChevronUpDown className='!icon-sm' />\n </Button>\n </>\n )}\n\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant='ghost' size='small' color='neutral' aria-label='Wallarm Updates'>\n <Bell />\n </Button>\n </TooltipTrigger>\n <TooltipContent>Wallarm updates</TooltipContent>\n </Tooltip>\n\n <QuickHelpDropdown />\n </TopHeaderActions>\n </TopHeader>\n </AppShellHeader>\n\n <AppShellRail>\n <NavRail collapsed={collapsed}>\n <NavRailBody>\n <NavRailItem\n icon={Home}\n label='Home'\n shortcut={['G', 'H']}\n active={activeProduct === 'home'}\n onClick={() => navigateToProduct('home')}\n />\n <RecentDropdown />\n\n <NavRailSeparator />\n\n {loading ? (\n <NavRailSkeleton />\n ) : (\n <>\n <NavRailItem\n icon={CircleDashed}\n label='Edge'\n shortcut={['G', 'E']}\n active={activeProduct === 'edge'}\n onClick={() => navigateToProduct('edge')}\n />\n <NavRailItem\n icon={CircleDashed}\n label='AI Hypervisor'\n shortcut={['G', 'A']}\n active={activeProduct === 'ai-hypervisor'}\n onClick={() => navigateToProduct('ai-hypervisor')}\n />\n <NavRailItem\n icon={CircleDashed}\n label='Infra Discovery'\n shortcut={['G', 'I']}\n active={activeProduct === 'infra-discovery'}\n onClick={() => navigateToProduct('infra-discovery')}\n />\n <NavRailItem\n icon={CircleDashed}\n label='Security Testing'\n shortcut={['G', 'T']}\n active={activeProduct === 'security-testing'}\n onClick={() => navigateToProduct('security-testing')}\n />\n </>\n )}\n </NavRailBody>\n\n <NavRailFooter>\n <NavRailItem\n icon={Settings}\n label='Settings'\n shortcut={['G', 'S']}\n active={activeProduct === 'settings'}\n onClick={() => navigateToProduct('settings')}\n />\n <AccountDropdown\n sidebarMode={sidebarMode}\n onSidebarModeChange={setSidebarMode}\n theme={theme}\n onThemeChange={setTheme}\n />\n </NavRailFooter>\n </NavRail>\n </AppShellRail>\n\n <AppShellRemote>\n <div className='flex gap-8 absolute top-4 right-4 z-10'>\n <Button variant='ghost' size='small' color='neutral' onClick={() => setLoading(v => !v)}>\n {loading ? 'Finish loading' : 'Start loading'}\n </Button>\n\n <Button\n variant='ghost'\n size='small'\n color='neutral'\n onClick={() => setRevealKey(k => k + 1)}\n >\n Replay animation\n </Button>\n </div>\n\n <RemoteForProduct product={activeProduct} />\n </AppShellRemote>\n </AppShell>\n );\n}"
4839
+ "code": "() => {\n const pathname = useLocationPathname();\n const activeProduct = deriveProduct(pathname);\n\n const [loading, setLoading] = useState(true);\n const [sidebarMode, setSidebarMode] = useState<SidebarMode>('adaptive');\n const { theme, setTheme } = useTheme();\n const collapsed = sidebarMode === 'adaptive' && activeProduct !== 'home';\n\n return (\n <AppShell>\n <AppShellHeader>\n <TopHeader>\n <TopHeaderLogo href='/'>\n <WallarmLogo />\n </TopHeaderLogo>\n\n <TopHeaderActions>\n {loading ? (\n <>\n <Skeleton width='150px' height='20px' rounded={6} />\n <TopHeaderSeparator />\n <Skeleton width='150px' height='20px' rounded={6} />\n\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n variant='ghost'\n size='small'\n color='neutral'\n aria-label='Wallarm Updates'\n >\n <Bell />\n </Button>\n </TooltipTrigger>\n <TooltipContent>Wallarm updates</TooltipContent>\n </Tooltip>\n\n <QuickHelpDropdown />\n </>\n ) : (\n <HeaderActions />\n )}\n </TopHeaderActions>\n </TopHeader>\n </AppShellHeader>\n\n <AppShellRail>\n <NavRail collapsed={collapsed}>\n <NavRailBody>\n <NavRailItem\n icon={Home}\n label='Home'\n shortcut={['G', 'H']}\n active={activeProduct === 'home'}\n onClick={() => navigateToProduct('home')}\n />\n <RecentDropdown />\n\n <NavRailSeparator />\n\n {loading ? <NavRailSkeleton /> : <ProductNavItems activeProduct={activeProduct} />}\n </NavRailBody>\n\n <NavRailFooterContent\n activeProduct={activeProduct}\n sidebarMode={sidebarMode}\n onSidebarModeChange={setSidebarMode}\n theme={theme}\n onThemeChange={setTheme}\n />\n </NavRail>\n </AppShellRail>\n\n <AppShellRemote>\n <div className='flex gap-8 absolute top-4 right-4 z-10'>\n <Button variant='ghost' size='small' color='neutral' onClick={() => setLoading(v => !v)}>\n {loading ? 'Finish loading' : 'Start loading'}\n </Button>\n </div>\n\n <RemoteForProduct product={activeProduct} />\n </AppShellRemote>\n </AppShell>\n );\n}"
4840
+ },
4841
+ {
4842
+ "name": "RevealFlow",
4843
+ "code": "() => {\n const pathname = useLocationPathname();\n const activeProduct = deriveProduct(pathname);\n\n const [splashDone, setSplashDone] = useState(false);\n const [sidebarMode, setSidebarMode] = useState<SidebarMode>('adaptive');\n const [revealKey, setRevealKey] = useState(0);\n const { theme, setTheme } = useTheme();\n const collapsed = sidebarMode === 'adaptive' && activeProduct !== 'home';\n\n useEffect(() => {\n if (splashDone) return;\n const timer = setTimeout(() => setSplashDone(true), 2000);\n return () => clearTimeout(timer);\n }, [splashDone]);\n\n const handleReplay = () => {\n setSplashDone(false);\n setRevealKey(k => k + 1);\n };\n\n if (!splashDone) {\n return (\n <div key={revealKey} className='h-screen w-screen bg-bg-page-bg'>\n <SplashScreen />\n </div>\n );\n }\n\n return (\n <AppShell key={revealKey} reveal>\n <AppShellHeader>\n <TopHeader>\n <TopHeaderLogo href='/'>\n <WallarmLogo />\n </TopHeaderLogo>\n\n <TopHeaderActions>\n <HeaderActions />\n </TopHeaderActions>\n </TopHeader>\n </AppShellHeader>\n\n <AppShellRail>\n <NavRail collapsed={collapsed}>\n <NavRailBody>\n <NavRailItem\n icon={Home}\n label='Home'\n shortcut={['G', 'H']}\n active={activeProduct === 'home'}\n onClick={() => navigateToProduct('home')}\n />\n <RecentDropdown />\n\n <NavRailSeparator />\n\n <ProductNavItems activeProduct={activeProduct} />\n </NavRailBody>\n\n <NavRailFooterContent\n activeProduct={activeProduct}\n sidebarMode={sidebarMode}\n onSidebarModeChange={setSidebarMode}\n theme={theme}\n onThemeChange={setTheme}\n />\n </NavRail>\n </AppShellRail>\n\n <AppShellRemote>\n <div className='flex gap-8 absolute top-4 right-4 z-10'>\n <Button variant='ghost' size='small' color='neutral' onClick={handleReplay}>\n Replay animation\n </Button>\n </div>\n\n <RemoteForProduct product={activeProduct} />\n </AppShellRemote>\n </AppShell>\n );\n}"
4844
+ },
4845
+ {
4846
+ "name": "LoginFlow",
4847
+ "code": "() => {\n const pathname = useLocationPathname();\n const activeProduct = deriveProduct(pathname);\n\n const [splashVisible, setSplashVisible] = useState(true);\n const [showShell, setShowShell] = useState(false);\n const [revealed, setRevealed] = useState(false);\n const [flowKey, setFlowKey] = useState(0);\n const [sidebarMode, setSidebarMode] = useState<SidebarMode>('adaptive');\n const { theme, setTheme } = useTheme();\n const collapsed = sidebarMode === 'adaptive' && activeProduct !== 'home';\n\n useEffect(() => {\n if (!splashVisible) return;\n const timer = setTimeout(() => setSplashVisible(false), 2000);\n return () => clearTimeout(timer);\n }, [splashVisible]);\n\n const handleSignIn = () => {\n setShowShell(true);\n };\n\n const handleReplay = () => {\n setShowShell(false);\n setRevealed(false);\n setSplashVisible(true);\n setFlowKey(k => k + 1);\n };\n\n return (\n <div\n key={flowKey}\n className='relative h-screen w-screen overflow-hidden bg-component-app-shell-bg'\n >\n {!revealed && (\n <AnimatedBackground\n className='absolute inset-0'\n style={{\n opacity: showShell ? 0 : 1,\n transition: showShell ? 'opacity 400ms cubic-bezier(0.4, 0, 0.2, 1)' : undefined,\n }}\n />\n )}\n\n {!showShell && (\n <div className='absolute inset-0 flex items-center justify-center z-10'>\n <SplashScreen\n visible={splashVisible}\n shrinkTarget={CARD_DIMENSIONS}\n className='bg-bg-page-bg shadow-lg'\n >\n <div className='flex h-full w-full flex-col items-center justify-center gap-16 p-24'>\n <Text size='xl'>Sign In</Text>\n <div className='flex w-full flex-col gap-12'>\n <Input placeholder='Email' />\n <Input placeholder='Password' type='password' />\n </div>\n <Button variant='primary' color='brand' className='w-full' onClick={handleSignIn}>\n Sign In\n </Button>\n </div>\n </SplashScreen>\n </div>\n )}\n\n {showShell && (\n <div className='absolute inset-0'>\n <AppShell expandFrom={CARD_DIMENSIONS} onRevealed={() => setRevealed(true)}>\n <AppShellHeader>\n <TopHeader>\n <TopHeaderLogo href='/'>\n <WallarmLogo />\n </TopHeaderLogo>\n\n <TopHeaderActions>\n <HeaderActions />\n </TopHeaderActions>\n </TopHeader>\n </AppShellHeader>\n\n <AppShellRail>\n <NavRail collapsed={collapsed}>\n <NavRailBody>\n <NavRailItem\n icon={Home}\n label='Home'\n active={activeProduct === 'home'}\n onClick={() => navigateToProduct('home')}\n />\n\n <NavRailSeparator />\n\n <ProductNavItems activeProduct={activeProduct} />\n </NavRailBody>\n <NavRailFooterContent\n activeProduct={activeProduct}\n sidebarMode={sidebarMode}\n onSidebarModeChange={setSidebarMode}\n theme={theme}\n onThemeChange={setTheme}\n />\n </NavRail>\n </AppShellRail>\n\n <AppShellRemote>\n <div className='flex gap-8 absolute top-4 right-4 z-10'>\n <Button variant='ghost' size='small' color='neutral' onClick={handleReplay}>\n Replay animation\n </Button>\n </div>\n <RemoteForProduct product={activeProduct} />\n </AppShellRemote>\n </AppShell>\n </div>\n )}\n </div>\n );\n}"
4835
4848
  }
4836
4849
  ]
4837
4850
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",