@skyhook-io/k8s-ui 1.14.6 → 1.14.7

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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/src/components/applications/ApplicationDetail.tsx +36 -4
  3. package/src/components/applications/ApplicationsView.tsx +2 -1
  4. package/src/components/audit/AuditAlerts.tsx +35 -37
  5. package/src/components/audit/AuditFindingsTable.tsx +23 -27
  6. package/src/components/charts/PrometheusChartsView.tsx +29 -10
  7. package/src/components/checks/ChecksView.tsx +56 -26
  8. package/src/components/gitops/GitOpsDetailLayout.tsx +12 -9
  9. package/src/components/issues/IssuesView.tsx +35 -54
  10. package/src/components/logs/LogCore.tsx +40 -9
  11. package/src/components/resources/ResourcesSidebar.tsx +66 -78
  12. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +21 -15
  13. package/src/components/resources/renderers/CompositionRenderer.tsx +30 -10
  14. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +16 -12
  15. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +10 -4
  16. package/src/components/resources/renderers/GenericRenderer.tsx +13 -7
  17. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +6 -4
  18. package/src/components/resources/renderers/PolicyCoverageSection.tsx +7 -4
  19. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +13 -15
  20. package/src/components/resources/renderers/SbomReportRenderer.tsx +10 -4
  21. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +10 -4
  22. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +13 -6
  23. package/src/components/shared/CreateResourceDialog.tsx +7 -4
  24. package/src/components/timeline/TimelineList.tsx +14 -11
  25. package/src/components/timeline/TimelineStrip.tsx +26 -4
  26. package/src/components/timeline/TimelineSwimlanes.tsx +3 -5
  27. package/src/components/timeline/TimelineToolbar.tsx +43 -10
  28. package/src/components/topology/TopologyControls.tsx +27 -8
  29. package/src/components/topology/TopologyGraph.tsx +14 -5
  30. package/src/components/trace/ReachabilityView.tsx +17 -16
  31. package/src/components/ui/CardSection.tsx +79 -35
  32. package/src/components/ui/Collapse.test.tsx +74 -0
  33. package/src/components/ui/Collapse.tsx +115 -29
  34. package/src/components/ui/DialogPortal.tsx +7 -2
  35. package/src/components/ui/ForceDeleteConfirmDialog.tsx +7 -4
  36. package/src/components/ui/RestrictedState.tsx +7 -4
  37. package/src/components/ui/RowActionMenu.tsx +20 -8
  38. package/src/components/ui/SelectMenu.tsx +18 -6
  39. package/src/components/ui/YamlEditor.tsx +7 -9
  40. package/src/components/ui/drawer-components.tsx +14 -21
  41. package/src/components/ui/index.ts +30 -22
  42. package/src/components/workload/WorkloadView.tsx +6 -10
  43. package/src/hooks/useAnimatedUnmount.ts +21 -3
  44. package/src/theme/components.css +4 -1
  45. package/src/utils/animation.ts +93 -13
@@ -1,10 +1,11 @@
1
1
  import { createContext, useContext, useState } from 'react'
2
- import { ChevronRight, Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers, X, Minus } from 'lucide-react'
2
+ import { Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers, X, Minus } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import { formatAge, formatDuration, formatResources } from '../resources/resource-utils'
5
5
  import { getEffectiveResources } from '../../utils/extended-resources'
6
6
  import { Tooltip } from './Tooltip'
7
7
  import { getKindColorClass } from '../ui/Badge'
8
+ import { Collapse, CollapseChevron, useDisclosure } from './Collapse'
8
9
 
9
10
  // ============================================================================
10
11
  // UI COMPONENTS
@@ -86,26 +87,22 @@ interface SectionProps {
86
87
 
87
88
  export function Section({ title, icon: Icon, children, defaultExpanded = true, contentClassName }: SectionProps) {
88
89
  const [expanded, setExpanded] = useState(defaultExpanded)
90
+ const { panelId, buttonProps } = useDisclosure(expanded)
89
91
 
90
92
  return (
91
93
  <div className="border-b-subtle pb-4 last:border-0">
92
94
  <button
95
+ {...buttonProps}
93
96
  onClick={() => setExpanded(!expanded)}
94
97
  className="flex items-center gap-2 w-full text-left mb-2 hover:text-theme-text-primary transition-colors"
95
98
  >
96
- <ChevronRight className={clsx('w-4 h-4 text-theme-text-tertiary transition-transform duration-200', expanded && 'rotate-90')} />
99
+ <CollapseChevron open={expanded} className="w-4 h-4" />
97
100
  {Icon && <Icon className="w-4 h-4 text-theme-text-secondary" />}
98
101
  <span className="text-sm font-medium text-theme-text-secondary">{title}</span>
99
102
  </button>
100
- <div
101
- className="grid transition-[grid-template-rows] duration-200 ease-out"
102
- style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
103
- >
104
- {/* `relative` is load-bearing — see the note in ui/Collapse.tsx. */}
105
- <div className="relative overflow-hidden">
106
- <div className={contentClassName ?? 'pl-6'}>{children}</div>
107
- </div>
108
- </div>
103
+ <Collapse open={expanded} id={panelId}>
104
+ <div className={contentClassName ?? 'pl-6'}>{children}</div>
105
+ </Collapse>
109
106
  </div>
110
107
  )
111
108
  }
@@ -118,25 +115,21 @@ interface ExpandableSectionProps {
118
115
 
119
116
  export function ExpandableSection({ title, children, defaultExpanded = true }: ExpandableSectionProps) {
120
117
  const [expanded, setExpanded] = useState(defaultExpanded)
118
+ const { panelId, buttonProps } = useDisclosure(expanded)
121
119
 
122
120
  return (
123
121
  <div>
124
122
  <button
123
+ {...buttonProps}
125
124
  onClick={() => setExpanded(!expanded)}
126
125
  className="flex items-center gap-2 text-sm text-theme-text-secondary hover:text-theme-text-primary transition-colors mb-1"
127
126
  >
128
- <ChevronRight className={clsx('w-3.5 h-3.5 transition-transform duration-200', expanded && 'rotate-90')} />
127
+ <CollapseChevron open={expanded} className="w-3.5 h-3.5" />
129
128
  {title}
130
129
  </button>
131
- <div
132
- className="grid transition-[grid-template-rows] duration-200 ease-out"
133
- style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
134
- >
135
- {/* `relative` is load-bearing — see the note in ui/Collapse.tsx. */}
136
- <div className="relative overflow-hidden">
137
- <div className="ml-5">{children}</div>
138
- </div>
139
- </div>
130
+ <Collapse open={expanded} id={panelId}>
131
+ <div className="ml-5">{children}</div>
132
+ </Collapse>
140
133
  </div>
141
134
  )
142
135
  }
@@ -26,7 +26,7 @@ export { ConfirmDialog } from './ConfirmDialog'
26
26
  export { HealthRing } from './HealthRing'
27
27
  export { MetricsChart, MetricsSparkline } from './MetricsChart'
28
28
  export * from './drawer-components'
29
- export { Collapse, CollapseChevron } from './Collapse'
29
+ export { Collapse, CollapseChevron, useDisclosure, disclosurePanelId } from './Collapse'
30
30
  export { ResourceBar } from './ResourceBar'
31
31
  export { ForceDeleteConfirmDialog } from './ForceDeleteConfirmDialog'
32
32
  export { InClusterConsentDialog } from './InClusterConsentDialog'
@@ -37,31 +37,39 @@ export { YamlEditor, YamlDiffEditor } from './YamlEditor'
37
37
  // pulling monaco-editor into the main bundle. Anything that mounts a bare
38
38
  // `Editor` must await this first, or @monaco-editor/react reaches for its CDN.
39
39
  export const ensureMonacoRuntime = () =>
40
- import('./monacoRuntime').then(({ ensureMonaco }) => ensureMonaco())
40
+ import("./monacoRuntime").then(({ ensureMonaco }) => ensureMonaco());
41
41
  export type {
42
42
  YamlDiagnostic,
43
43
  YamlDocumentIdentity,
44
44
  YamlEditorProps,
45
45
  YamlSchemaLoader,
46
46
  YamlSchemaLoadResult,
47
- } from './YamlEditor'
48
- export { YamlReview } from './YamlReview'
49
- export type { YamlPreviewResult, YamlReviewProps } from './YamlReview'
50
- export { RowActionMenu } from './RowActionMenu'
51
- export type { RowActionItem } from './RowActionMenu'
47
+ } from "./YamlEditor";
48
+ export { YamlReview } from "./YamlReview";
49
+ export type { YamlPreviewResult, YamlReviewProps } from "./YamlReview";
50
+ export { RowActionMenu } from "./RowActionMenu";
51
+ export type { RowActionItem } from "./RowActionMenu";
52
52
 
53
- export { PageHeader } from './PageHeader'
54
- export { SummaryTile } from './SummaryTile'
55
- export type { SummaryTone } from './SummaryTile'
56
- export { Facet, FacetSection, FacetButton } from './Facet'
57
- export type { FacetTone } from './Facet'
58
- export { SortableTh, TH_CLASS } from './SortableTh'
59
- export type { SortDir } from './SortableTh'
60
- export { DistributionBar, DistributionLegendChip } from './DistributionBar'
61
- export type { DistributionSegment } from './DistributionBar'
62
- export { SearchPillInput } from './SearchPillInput'
63
- export type { SearchModifier, SearchPillInputProps } from './SearchPillInput'
64
- export { CardSection, CardBody, TerminalBlock, renderProse, NEUTRAL_CHIP_CLASS, KIND_CHIP_CLASS } from './CardSection'
65
- export type { CardSectionTone } from './CardSection'
66
- export { MultiSelectPicker } from './MultiSelectPicker'
67
- export type { MultiSelectPickerProps } from './MultiSelectPicker'
53
+ export { PageHeader } from "./PageHeader";
54
+ export { SummaryTile } from "./SummaryTile";
55
+ export type { SummaryTone } from "./SummaryTile";
56
+ export { Facet, FacetSection, FacetButton } from "./Facet";
57
+ export type { FacetTone } from "./Facet";
58
+ export { SortableTh, TH_CLASS } from "./SortableTh";
59
+ export type { SortDir } from "./SortableTh";
60
+ export { DistributionBar, DistributionLegendChip } from "./DistributionBar";
61
+ export type { DistributionSegment } from "./DistributionBar";
62
+ export { SearchPillInput } from "./SearchPillInput";
63
+ export type { SearchModifier, SearchPillInputProps } from "./SearchPillInput";
64
+ export {
65
+ CardSection,
66
+ CardBody,
67
+ TerminalBlock,
68
+ TerminalBlockLabel,
69
+ renderProse,
70
+ NEUTRAL_CHIP_CLASS,
71
+ KIND_CHIP_CLASS,
72
+ } from "./CardSection";
73
+ export type { CardSectionTone } from "./CardSection";
74
+ export { MultiSelectPicker } from "./MultiSelectPicker";
75
+ export type { MultiSelectPickerProps } from "./MultiSelectPicker";
@@ -7,6 +7,7 @@ import { FetchResult } from '../ui/FetchResult'
7
7
  import { PaneLoader } from '../ui/PaneLoader'
8
8
  import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
9
9
  import { clsx } from 'clsx'
10
+ import { Collapse } from '../ui/Collapse'
10
11
  import {
11
12
  ArrowLeft,
12
13
  ArrowRight,
@@ -2949,20 +2950,15 @@ function PodListFrame({
2949
2950
  {children}
2950
2951
  </div>
2951
2952
  {hasOverflow && (
2952
- <div
2953
- className={clsx(
2954
- 'grid transition-[grid-template-rows,opacity] duration-200',
2955
- expanded ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0',
2956
- )}
2957
- style={{ transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }}
2958
- aria-hidden={!expanded || undefined}
2959
- >
2960
- <div className={clsx('min-h-0 overflow-hidden', expanded && 'max-h-[26rem] overflow-y-auto pr-1')} inert={!expanded || undefined}>
2953
+ <Collapse open={expanded}>
2954
+ {/* Long overflow lists scroll inside a capped box once open; the
2955
+ cap comes off while closed so the collapse measures to zero. */}
2956
+ <div className={clsx(expanded && 'max-h-[26rem] overflow-y-auto pr-1')}>
2961
2957
  <div className="space-y-2">
2962
2958
  {overflow}
2963
2959
  </div>
2964
2960
  </div>
2965
- </div>
2961
+ </Collapse>
2966
2962
  )}
2967
2963
  {toggle}
2968
2964
  </div>
@@ -1,10 +1,15 @@
1
1
  import { useState, useEffect, useRef } from 'react'
2
- import { DURATION_NORMAL } from '../utils/animation'
2
+ import { DURATION_NORMAL, prefersReducedMotion } from '../utils/animation'
3
3
 
4
4
  /**
5
5
  * Delays React unmount until exit animation completes.
6
6
  * - `shouldRender`: whether the component should be in the DOM
7
7
  * - `isOpen`: drives CSS transition classes (false → closed state, true → open state)
8
+ *
9
+ * Pass the EXIT duration (overlayExitMs('dialog') etc.) — that is how long the
10
+ * closed state stays mounted. Under reduced motion the app stylesheet zeroes the
11
+ * transition, so the hook disposes at logical close instead of holding an
12
+ * invisible overlay (and its focus / scroll lock) for the exit window.
8
13
  */
9
14
  export function useAnimatedUnmount(isVisible: boolean, duration = DURATION_NORMAL) {
10
15
  const [shouldRender, setShouldRender] = useState(isVisible)
@@ -21,8 +26,21 @@ export function useAnimatedUnmount(isVisible: boolean, duration = DURATION_NORMA
21
26
  return () => cancelAnimationFrame(rafRef.current)
22
27
  } else if (shouldRender) {
23
28
  setIsOpen(false)
24
- const t = setTimeout(() => setShouldRender(false), duration)
25
- return () => clearTimeout(t)
29
+ if (prefersReducedMotion()) {
30
+ setShouldRender(false)
31
+ return
32
+ }
33
+ // Start the disposal clock only once the closed state has painted (same
34
+ // double rAF as the open path). A heavy overlay can take a slow frame to
35
+ // commit its closed classes; a timer started at logical close would then
36
+ // unmount it before the exit transition has had its full duration.
37
+ let t = 0
38
+ rafRef.current = requestAnimationFrame(() => {
39
+ rafRef.current = requestAnimationFrame(() => {
40
+ t = window.setTimeout(() => setShouldRender(false), duration)
41
+ })
42
+ })
43
+ return () => { cancelAnimationFrame(rafRef.current); clearTimeout(t) }
26
44
  }
27
45
  }, [isVisible, duration]) // eslint-disable-line react-hooks/exhaustive-deps — shouldRender intentionally omitted to avoid re-triggering cleanup
28
46
 
@@ -144,11 +144,14 @@
144
144
 
145
145
  /* ── ISSUE ROW DETAILS ── */
146
146
 
147
+ /* Legacy: prefer <Collapse unmountOnExit> (ui/Collapse.tsx). Kept for any
148
+ straggler; timing mirrors DURATION_DISCLOSURE / CSS_EASE in
149
+ utils/animation.ts — the source-hygiene test keeps them equal. */
147
150
  .issue-details-motion {
148
151
  display: grid;
149
152
  grid-template-rows: 0fr;
150
153
  overflow: hidden;
151
- transition: grid-template-rows 200ms ease-out;
154
+ transition: grid-template-rows 300ms cubic-bezier(0.32, 0.72, 0, 1);
152
155
  }
153
156
 
154
157
  .issue-details-motion-open {
@@ -1,27 +1,57 @@
1
1
  /**
2
2
  * Central animation presets for consistent motion across the UI.
3
3
  *
4
- * All animations use GPU-composited properties only (translate, scale, opacity)
5
- * to guarantee 60fps. The iOS-like spring easing gives a fast-in, gentle-out feel.
4
+ * One curve, a handful of durations, and class fragments that apply them.
5
+ * Every surface that opens, closes, expands or collapses takes its timing
6
+ * from here — the hub (radar-hub-web) re-exports this module so Radar Cloud
7
+ * chrome and the embedded Radar views move alike.
8
+ *
9
+ * Property choice: geometry that has to move (grid-template-rows on a
10
+ * disclosure, width on a drawer) is animated directly; everything else is
11
+ * transform + opacity so it stays GPU-composited.
6
12
  */
7
13
 
8
14
  // -- Easing ------------------------------------------------------------------
9
15
 
16
+ /** The one UI curve. iOS-style decelerate: fast out of the gate, gentle
17
+ * settle. Used for disclosures, menus, dialogs, sheets and drawers alike so
18
+ * a click reads as the cause of the motion and the content is legible early.
19
+ * Chosen over Material's standard curve in a side-by-side on real content. */
20
+ export const CSS_EASE = 'cubic-bezier(0.32, 0.72, 0, 1)'
21
+ /** Alias with the intent in the name; prefer it in new code. */
22
+ export const EASE_UI = CSS_EASE
23
+
10
24
  // Tailwind-safe easing (no spaces — Tailwind splits class strings on whitespace)
11
25
  const TW_EASE = 'ease-[cubic-bezier(0.32,0.72,0,1)]'
12
-
13
- /** Raw CSS easing for inline transition strings */
14
- export const CSS_EASE = 'cubic-bezier(0.32, 0.72, 0, 1)'
26
+ /** The same curve as a Tailwind class fragment, for class-string composition. */
27
+ export const TW_EASE_UI = TW_EASE
15
28
 
16
29
  // -- Durations (ms) ----------------------------------------------------------
17
30
 
18
- /** Standard UI transitions: overlays, panels, toasts */
31
+ /** Standard UI transitions: drawers, toasts. */
19
32
  export const DURATION_NORMAL = 300
20
- /** Dock height, subtle layout shifts */
33
+ /** Dock height, subtle layout shifts. Deliberately fast: the dock is direct
34
+ * manipulation the user performs dozens of times a day. */
21
35
  export const DURATION_DOCK = 150
22
36
  /** Toast exit animation */
23
37
  export const DURATION_TOAST_EXIT = 200
24
38
 
39
+ /** In-flow disclosure (Collapse / drawer sections / row expanders): panel
40
+ * and caret share this, open and close alike. Symmetric on purpose — an
41
+ * interrupted toggle reverses cleanly when both directions run one clock. */
42
+ export const DURATION_DISCLOSURE = 300
43
+
44
+ /** Menus, popovers, listboxes. Exits are shorter than entrances: a dismissal
45
+ * should feel immediate. */
46
+ export const DURATION_MENU_IN = 140
47
+ export const DURATION_MENU_OUT = 100
48
+ /** Modal dialogs and command palettes. */
49
+ export const DURATION_DIALOG_IN = 220
50
+ export const DURATION_DIALOG_OUT = 160
51
+ /** Bottom sheets: more travel, a little more time. */
52
+ export const DURATION_SHEET_IN = 280
53
+ export const DURATION_SHEET_OUT = 220
54
+
25
55
  /** Drawer ↔ fullscreen expand/collapse morph. Longer than DURATION_NORMAL so a
26
56
  * large width change advances in small per-frame steps (reads as fluid, not
27
57
  * abrupt). Drives the frame width, the content crossfade, and the JS window
@@ -32,19 +62,69 @@ export const DURATION_DRAWER_MORPH = 260
32
62
  * crossfade fixed the reflow that originally needed the gentle start. */
33
63
  export const EASE_DRAWER_MORPH = 'cubic-bezier(0.2, 0.8, 0.2, 1)'
34
64
 
65
+ // -- Reduced motion -----------------------------------------------------------
66
+
67
+ /** True when the OS asks for reduced motion. Presence hooks use it to dispose
68
+ * overlays at logical close instead of after an exit transition that the app
69
+ * stylesheet has already zeroed. SSR- and test-safe. */
70
+ export function prefersReducedMotion(): boolean {
71
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false
72
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches
73
+ }
74
+
35
75
  // -- Tailwind class presets ---------------------------------------------------
36
76
  // Reusable class fragments — import and spread into clsx() calls.
37
77
 
78
+ /** Disclosure panel: grid-template-rows 0fr ↔ 1fr on the wrapper. Pair with
79
+ * TRANSITION_CHEVRON on the caret so both run one clock. */
80
+ export const TRANSITION_DISCLOSURE =
81
+ `transition-[grid-template-rows] duration-300 ${TW_EASE} motion-reduce:transition-none`
82
+
83
+ /** Disclosure caret (rotate). Same duration and curve as the panel it opens. */
84
+ export const TRANSITION_CHEVRON =
85
+ `transition-transform duration-300 ${TW_EASE} motion-reduce:transition-none`
86
+
38
87
  /** Drawer slide from right — use with translate-x-0/translate-x-full + opacity.
39
88
  * Also handles expand/collapse (width) so a single transition covers both. */
40
89
  export const TRANSITION_DRAWER =
41
- `transition-[translate,opacity,width] duration-300 ${TW_EASE} will-change-[transform,width]`
90
+ `transition-[translate,opacity,width] duration-300 ${TW_EASE} will-change-[transform,width] motion-reduce:transition-none`
42
91
 
43
-
44
- /** Overlay backdrop fade */
92
+ /** Overlay backdrop fade. Duration comes from the presence state (see
93
+ * overlayTransitionStyle) so the exit can be shorter than the entrance. */
45
94
  export const TRANSITION_BACKDROP =
46
- 'transition-opacity duration-200'
95
+ `transition-opacity ${TW_EASE} motion-reduce:transition-none`
47
96
 
48
- /** Overlay panel scale + fade — use with scale-100/scale-[0.97] + opacity */
97
+ /** Overlay panel scale + fade — use with scale-100/scale-[0.97] + opacity.
98
+ * Duration comes from overlayTransitionStyle, not from this fragment. */
49
99
  export const TRANSITION_PANEL =
50
- `transition-[translate,scale,opacity] duration-300 ${TW_EASE}`
100
+ `transition-[translate,scale,opacity] ${TW_EASE} motion-reduce:transition-none`
101
+
102
+ /** Menu / popover: translate + scale + opacity from its anchor corner. Pair
103
+ * with a transform-origin utility so it grows from the trigger. */
104
+ export const TRANSITION_MENU =
105
+ `transition-[translate,scale,opacity] ${TW_EASE} motion-reduce:transition-none`
106
+
107
+ export type OverlayKind = 'menu' | 'dialog' | 'sheet' | 'drawer'
108
+
109
+ const OVERLAY_DURATIONS: Record<OverlayKind, { in: number; out: number }> = {
110
+ menu: { in: DURATION_MENU_IN, out: DURATION_MENU_OUT },
111
+ dialog: { in: DURATION_DIALOG_IN, out: DURATION_DIALOG_OUT },
112
+ sheet: { in: DURATION_SHEET_IN, out: DURATION_SHEET_OUT },
113
+ // Side drawers keep their symmetric slide (TRANSITION_DRAWER); this entry
114
+ // gives their backdrop the same clock.
115
+ drawer: { in: DURATION_NORMAL, out: DURATION_NORMAL },
116
+ }
117
+
118
+ /** Exit duration for an overlay kind — what useAnimatedUnmount should wait. */
119
+ export function overlayExitMs(kind: OverlayKind): number {
120
+ return OVERLAY_DURATIONS[kind].out
121
+ }
122
+
123
+ /** Inline style that gives an overlay its entrance or exit duration. Apply to
124
+ * the element that carries TRANSITION_PANEL / TRANSITION_MENU / TRANSITION_BACKDROP;
125
+ * the class fragments deliberately carry no duration so the two directions
126
+ * can differ. */
127
+ export function overlayTransitionStyle(isOpen: boolean, kind: OverlayKind): { transitionDuration: string } {
128
+ const d = OVERLAY_DURATIONS[kind]
129
+ return { transitionDuration: `${isOpen ? d.in : d.out}ms` }
130
+ }