@skyhook-io/k8s-ui 1.14.6 → 1.14.8
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.
- package/package.json +1 -1
- package/src/components/applications/ApplicationDetail.tsx +36 -4
- package/src/components/applications/ApplicationsView.tsx +2 -1
- package/src/components/audit/AuditAlerts.tsx +35 -37
- package/src/components/audit/AuditFindingsTable.tsx +23 -27
- package/src/components/charts/PrometheusChartsView.tsx +29 -10
- package/src/components/checks/ChecksView.tsx +56 -26
- package/src/components/gitops/GitOpsDetailLayout.tsx +12 -9
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +92 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +36 -6
- package/src/components/issues/IssuesView.tsx +35 -54
- package/src/components/logs/LogCore.tsx +40 -9
- package/src/components/resources/ResourcesSidebar.tsx +66 -78
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +21 -15
- package/src/components/resources/renderers/CompositionRenderer.tsx +30 -10
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +16 -12
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/GenericRenderer.tsx +13 -7
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +6 -4
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +7 -4
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +13 -15
- package/src/components/resources/renderers/SbomReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +13 -6
- package/src/components/shared/CreateResourceDialog.tsx +7 -4
- package/src/components/timeline/TimelineList.tsx +14 -11
- package/src/components/timeline/TimelineStrip.tsx +26 -4
- package/src/components/timeline/TimelineSwimlanes.tsx +3 -5
- package/src/components/timeline/TimelineToolbar.tsx +43 -10
- package/src/components/topology/K8sResourceNode.tsx +4 -2
- package/src/components/topology/TopologyControls.tsx +27 -8
- package/src/components/topology/TopologyGraph.tsx +14 -5
- package/src/components/trace/ReachabilityView.tsx +17 -16
- package/src/components/ui/CardSection.tsx +79 -35
- package/src/components/ui/Collapse.test.tsx +61 -0
- package/src/components/ui/Collapse.tsx +115 -29
- package/src/components/ui/DialogPortal.tsx +7 -2
- package/src/components/ui/Disclosure.test.tsx +26 -0
- package/src/components/ui/Disclosure.tsx +69 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +7 -4
- package/src/components/ui/RestrictedState.tsx +7 -4
- package/src/components/ui/RowActionMenu.tsx +20 -8
- package/src/components/ui/SelectMenu.tsx +18 -6
- package/src/components/ui/YamlEditor.tsx +7 -9
- package/src/components/ui/drawer-components.tsx +14 -21
- package/src/components/ui/index.ts +31 -22
- package/src/components/workload/WorkloadView.tsx +6 -10
- package/src/hooks/useAnimatedUnmount.ts +21 -3
- package/src/theme/components.css +0 -19
- package/src/utils/animation.ts +93 -13
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
|
-
import { Shield,
|
|
3
|
+
import { Shield, Copy, Check } from 'lucide-react'
|
|
4
|
+
import { Collapse, CollapseChevron, useDisclosure } from './Collapse'
|
|
4
5
|
|
|
5
6
|
// RestrictedState is the shared "you can't see this because of Kubernetes RBAC"
|
|
6
7
|
// surface — distinct from EmptyState (which covers healthy / filtered / no-data).
|
|
@@ -61,6 +62,7 @@ function buildRbacRequest(group: string, resource: string): string {
|
|
|
61
62
|
|
|
62
63
|
export function RestrictedState({ kindLabel, group = '', resource, reason, compact, className }: Props) {
|
|
63
64
|
const [expanded, setExpanded] = useState(false)
|
|
65
|
+
const accessDisclosure = useDisclosure(expanded)
|
|
64
66
|
const [copied, setCopied] = useState(false)
|
|
65
67
|
const isUnavailable = reason === 'unavailable'
|
|
66
68
|
|
|
@@ -112,14 +114,15 @@ export function RestrictedState({ kindLabel, group = '', resource, reason, compa
|
|
|
112
114
|
|
|
113
115
|
<div className="mt-3 w-full max-w-md">
|
|
114
116
|
<button
|
|
117
|
+
{...accessDisclosure.buttonProps}
|
|
115
118
|
onClick={() => setExpanded((v) => !v)}
|
|
116
119
|
className="flex items-center gap-1.5 mx-auto text-sm text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
117
120
|
>
|
|
118
|
-
<
|
|
121
|
+
<CollapseChevron open={expanded} className="w-4 h-4" />
|
|
119
122
|
How to get access
|
|
120
123
|
</button>
|
|
121
124
|
|
|
122
|
-
{expanded
|
|
125
|
+
<Collapse open={expanded} id={accessDisclosure.panelId}>
|
|
123
126
|
<div className="mt-2 text-left">
|
|
124
127
|
<p className="text-xs text-theme-text-tertiary mb-2">
|
|
125
128
|
Apply this, or send it to whoever administers your cluster, to grant your identity
|
|
@@ -143,7 +146,7 @@ export function RestrictedState({ kindLabel, group = '', resource, reason, compa
|
|
|
143
146
|
</pre>
|
|
144
147
|
</div>
|
|
145
148
|
</div>
|
|
146
|
-
|
|
149
|
+
</Collapse>
|
|
147
150
|
</div>
|
|
148
151
|
</>
|
|
149
152
|
)}
|
|
@@ -2,6 +2,8 @@ import { Fragment, useEffect, useLayoutEffect, useRef, useState, type ComponentT
|
|
|
2
2
|
import { Loader2, MoreVertical } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Tooltip } from './Tooltip'
|
|
5
|
+
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
6
|
+
import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
|
|
5
7
|
|
|
6
8
|
export interface RowActionItem {
|
|
7
9
|
key: string
|
|
@@ -25,20 +27,21 @@ interface RowActionMenuProps {
|
|
|
25
27
|
|
|
26
28
|
export function RowActionMenu({ items, ariaLabel = 'Row actions', compact = true }: RowActionMenuProps) {
|
|
27
29
|
const [open, setOpen] = useState(false)
|
|
30
|
+
const { shouldRender, isOpen } = useAnimatedUnmount(open, overlayExitMs('menu'))
|
|
28
31
|
// Flip the menu above the trigger when it would otherwise spill past the
|
|
29
32
|
// viewport bottom. The GitOps table's bottom rows sit at the end of a scroll
|
|
30
33
|
// container with the app's fixed overlay buttons below them, so a
|
|
31
34
|
// downward-opening menu there clips its lowest items with no way to scroll
|
|
32
|
-
// them into view. Measured
|
|
35
|
+
// them into view. Measured once the menu is mounted (useLayoutEffect,
|
|
36
|
+
// pre-paint, no flicker). The last placement is kept through the exit
|
|
37
|
+
// transition — resetting at logical close would snap the menu to the other
|
|
38
|
+
// side mid-fade — and re-measured on the next open.
|
|
33
39
|
const [openUp, setOpenUp] = useState(false)
|
|
34
40
|
const ref = useRef<HTMLDivElement>(null)
|
|
35
41
|
const menuRef = useRef<HTMLDivElement>(null)
|
|
36
42
|
|
|
37
43
|
useLayoutEffect(() => {
|
|
38
|
-
if (!open)
|
|
39
|
-
setOpenUp(false)
|
|
40
|
-
return
|
|
41
|
-
}
|
|
44
|
+
if (!open || !shouldRender) return
|
|
42
45
|
const trigger = ref.current?.getBoundingClientRect()
|
|
43
46
|
const menuH = menuRef.current?.offsetHeight ?? 0
|
|
44
47
|
if (!trigger) return
|
|
@@ -46,7 +49,7 @@ export function RowActionMenu({ items, ariaLabel = 'Row actions', compact = true
|
|
|
46
49
|
// Flip up only when there's not enough room below AND enough room above,
|
|
47
50
|
// so a tall menu near the top doesn't get clipped at the other end.
|
|
48
51
|
setOpenUp(menuH + 8 > spaceBelow && trigger.top > menuH + 8)
|
|
49
|
-
}, [open])
|
|
52
|
+
}, [open, shouldRender])
|
|
50
53
|
|
|
51
54
|
useEffect(() => {
|
|
52
55
|
if (!open) return
|
|
@@ -89,14 +92,23 @@ export function RowActionMenu({ items, ariaLabel = 'Row actions', compact = true
|
|
|
89
92
|
>
|
|
90
93
|
<MoreVertical className={iconSize} />
|
|
91
94
|
</button>
|
|
92
|
-
{
|
|
95
|
+
{shouldRender && (
|
|
93
96
|
<div
|
|
94
97
|
ref={menuRef}
|
|
95
98
|
role="menu"
|
|
99
|
+
inert={!open}
|
|
96
100
|
className={clsx(
|
|
97
101
|
'absolute right-0 z-50 min-w-[180px] rounded-lg border border-theme-border bg-theme-surface py-1 shadow-xl',
|
|
98
|
-
openUp ? 'bottom-full mb-1' : 'top-full mt-1',
|
|
102
|
+
openUp ? 'bottom-full mb-1 origin-bottom-right' : 'top-full mt-1 origin-top-right',
|
|
103
|
+
TRANSITION_MENU,
|
|
104
|
+
isOpen
|
|
105
|
+
? 'opacity-100 translate-y-0 scale-100'
|
|
106
|
+
: openUp
|
|
107
|
+
? 'opacity-0 translate-y-1 scale-[0.97]'
|
|
108
|
+
: 'opacity-0 -translate-y-1 scale-[0.97]',
|
|
109
|
+
!open && 'pointer-events-none',
|
|
99
110
|
)}
|
|
111
|
+
style={overlayTransitionStyle(isOpen, 'menu')}
|
|
100
112
|
onClick={(e) => e.stopPropagation()}
|
|
101
113
|
>
|
|
102
114
|
{items.map((item) => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { Check, ChevronDown, Search } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
+
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
5
|
+
import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
|
|
4
6
|
|
|
5
7
|
export interface SelectMenuOption {
|
|
6
8
|
value: string
|
|
@@ -29,6 +31,7 @@ export function SelectMenu({
|
|
|
29
31
|
id?: string
|
|
30
32
|
}) {
|
|
31
33
|
const [open, setOpen] = useState(false)
|
|
34
|
+
const { shouldRender, isOpen } = useAnimatedUnmount(open, overlayExitMs('menu'))
|
|
32
35
|
const [query, setQuery] = useState('')
|
|
33
36
|
const [highlightedIndex, setHighlightedIndex] = useState(0)
|
|
34
37
|
const listboxId = useId()
|
|
@@ -62,17 +65,21 @@ export function SelectMenu({
|
|
|
62
65
|
}
|
|
63
66
|
}, [open])
|
|
64
67
|
|
|
68
|
+
// Reset once the list has actually left the DOM — clearing at logical close
|
|
69
|
+
// would re-filter the options mid-fade.
|
|
65
70
|
useEffect(() => {
|
|
66
|
-
if (!
|
|
67
|
-
}, [
|
|
71
|
+
if (!shouldRender) setQuery('')
|
|
72
|
+
}, [shouldRender])
|
|
68
73
|
|
|
69
74
|
useEffect(() => {
|
|
70
75
|
if (!open || !query) return
|
|
71
76
|
if (listRef.current) listRef.current.scrollTop = 0
|
|
72
77
|
}, [open, query])
|
|
73
78
|
|
|
79
|
+
// The list mounts a commit after `open` (presence hook), so this waits for
|
|
80
|
+
// `shouldRender` too — on `open` alone listRef is still null.
|
|
74
81
|
useEffect(() => {
|
|
75
|
-
if (!open) return
|
|
82
|
+
if (!open || !shouldRender) return
|
|
76
83
|
if (!searchPlaceholder) {
|
|
77
84
|
triggerRef.current?.focus()
|
|
78
85
|
return
|
|
@@ -80,7 +87,7 @@ export function SelectMenu({
|
|
|
80
87
|
listRef.current
|
|
81
88
|
?.querySelector<HTMLElement>('[role="option"][aria-selected="true"]')
|
|
82
89
|
?.scrollIntoView({ block: 'nearest' })
|
|
83
|
-
}, [open, searchPlaceholder])
|
|
90
|
+
}, [open, shouldRender, searchPlaceholder])
|
|
84
91
|
|
|
85
92
|
return (
|
|
86
93
|
<div
|
|
@@ -139,12 +146,17 @@ export function SelectMenu({
|
|
|
139
146
|
<span className="truncate">{selected?.label}</span>
|
|
140
147
|
<ChevronDown className={clsx('h-3.5 w-3.5 shrink-0 text-theme-text-tertiary transition-transform', open && 'rotate-180')} />
|
|
141
148
|
</button>
|
|
142
|
-
{
|
|
149
|
+
{shouldRender && (
|
|
143
150
|
<div
|
|
151
|
+
inert={!open}
|
|
144
152
|
className={clsx(
|
|
145
153
|
'absolute top-full z-50 mt-1 min-w-full overflow-hidden rounded-md border border-theme-border bg-theme-surface shadow-theme-lg',
|
|
146
|
-
searchPlaceholder ? 'left-0 right-0' : 'right-0'
|
|
154
|
+
searchPlaceholder ? 'left-0 right-0 origin-top-left' : 'right-0 origin-top-right',
|
|
155
|
+
TRANSITION_MENU,
|
|
156
|
+
isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]',
|
|
157
|
+
!open && 'pointer-events-none',
|
|
147
158
|
)}
|
|
159
|
+
style={overlayTransitionStyle(isOpen, 'menu')}
|
|
148
160
|
>
|
|
149
161
|
{searchPlaceholder && (
|
|
150
162
|
<div
|
|
@@ -7,7 +7,8 @@ import Editor, {
|
|
|
7
7
|
type OnMount,
|
|
8
8
|
} from '@monaco-editor/react'
|
|
9
9
|
import type { editor } from 'monaco-editor'
|
|
10
|
-
import { AlertCircle, AlertTriangle,
|
|
10
|
+
import { AlertCircle, AlertTriangle, Info } from 'lucide-react'
|
|
11
|
+
import { Collapse, CollapseChevron, useDisclosure } from './Collapse'
|
|
11
12
|
import { parseAllDocuments, parseDocument } from 'yaml'
|
|
12
13
|
import { isMac } from '../../utils/platform'
|
|
13
14
|
import { splitYamlDocuments } from '../../utils/yaml'
|
|
@@ -228,6 +229,7 @@ export function YamlEditor({
|
|
|
228
229
|
const [runtimeAttempt, setRuntimeAttempt] = useState(0)
|
|
229
230
|
const [diagnostics, setDiagnostics] = useState<YamlDiagnostic[]>([])
|
|
230
231
|
const [problemsOpen, setProblemsOpen] = useState(false)
|
|
232
|
+
const problemsDisclosure = useDisclosure(problemsOpen)
|
|
231
233
|
const [schemaStatus, setSchemaStatus] = useState<SchemaStatus>(schemaLoader ? 'loading' : 'idle')
|
|
232
234
|
const [schemaMessage, setSchemaMessage] = useState('')
|
|
233
235
|
const [schemaUnavailable, setSchemaUnavailable] = useState<
|
|
@@ -613,16 +615,12 @@ export function YamlEditor({
|
|
|
613
615
|
{showProblems && (
|
|
614
616
|
<div className="shrink-0 border-t border-theme-border bg-theme-elevated/60">
|
|
615
617
|
<button
|
|
618
|
+
{...problemsDisclosure.buttonProps}
|
|
616
619
|
type="button"
|
|
617
620
|
onClick={() => setProblemsOpen((open) => !open)}
|
|
618
621
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-theme-text-secondary hover:bg-theme-hover"
|
|
619
|
-
aria-expanded={problemsOpen}
|
|
620
622
|
>
|
|
621
|
-
{problemsOpen
|
|
622
|
-
<ChevronDown className="h-3.5 w-3.5" />
|
|
623
|
-
) : (
|
|
624
|
-
<ChevronRight className="h-3.5 w-3.5" />
|
|
625
|
-
)}
|
|
623
|
+
<CollapseChevron open={problemsOpen} className="h-3.5 w-3.5" />
|
|
626
624
|
<span className="font-medium text-theme-text-primary">Problems</span>
|
|
627
625
|
<span>{problemSummary}</span>
|
|
628
626
|
<span className="ml-auto flex min-w-0 items-center gap-3 text-theme-text-tertiary">
|
|
@@ -646,7 +644,7 @@ export function YamlEditor({
|
|
|
646
644
|
)}
|
|
647
645
|
</span>
|
|
648
646
|
</button>
|
|
649
|
-
{problemsOpen
|
|
647
|
+
<Collapse open={problemsOpen} id={problemsDisclosure.panelId}>
|
|
650
648
|
<div className="max-h-36 overflow-auto border-t border-theme-border py-1">
|
|
651
649
|
{diagnostics.length === 0 && schemaUnavailable.length === 0 ? (
|
|
652
650
|
<div className="px-3 py-2 text-xs text-theme-text-tertiary">
|
|
@@ -699,7 +697,7 @@ export function YamlEditor({
|
|
|
699
697
|
)
|
|
700
698
|
})}
|
|
701
699
|
</div>
|
|
702
|
-
|
|
700
|
+
</Collapse>
|
|
703
701
|
</div>
|
|
704
702
|
)}
|
|
705
703
|
</div>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createContext, useContext, useState } from 'react'
|
|
2
|
-
import {
|
|
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
|
-
<
|
|
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
|
-
<
|
|
101
|
-
className=
|
|
102
|
-
|
|
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
|
-
<
|
|
127
|
+
<CollapseChevron open={expanded} className="w-3.5 h-3.5" />
|
|
129
128
|
{title}
|
|
130
129
|
</button>
|
|
131
|
-
<
|
|
132
|
-
className="
|
|
133
|
-
|
|
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,8 @@ 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
|
+
export { Disclosure } from './Disclosure'
|
|
30
31
|
export { ResourceBar } from './ResourceBar'
|
|
31
32
|
export { ForceDeleteConfirmDialog } from './ForceDeleteConfirmDialog'
|
|
32
33
|
export { InClusterConsentDialog } from './InClusterConsentDialog'
|
|
@@ -37,31 +38,39 @@ export { YamlEditor, YamlDiffEditor } from './YamlEditor'
|
|
|
37
38
|
// pulling monaco-editor into the main bundle. Anything that mounts a bare
|
|
38
39
|
// `Editor` must await this first, or @monaco-editor/react reaches for its CDN.
|
|
39
40
|
export const ensureMonacoRuntime = () =>
|
|
40
|
-
import(
|
|
41
|
+
import("./monacoRuntime").then(({ ensureMonaco }) => ensureMonaco());
|
|
41
42
|
export type {
|
|
42
43
|
YamlDiagnostic,
|
|
43
44
|
YamlDocumentIdentity,
|
|
44
45
|
YamlEditorProps,
|
|
45
46
|
YamlSchemaLoader,
|
|
46
47
|
YamlSchemaLoadResult,
|
|
47
|
-
} from
|
|
48
|
-
export { YamlReview } from
|
|
49
|
-
export type { YamlPreviewResult, YamlReviewProps } from
|
|
50
|
-
export { RowActionMenu } from
|
|
51
|
-
export type { RowActionItem } from
|
|
48
|
+
} from "./YamlEditor";
|
|
49
|
+
export { YamlReview } from "./YamlReview";
|
|
50
|
+
export type { YamlPreviewResult, YamlReviewProps } from "./YamlReview";
|
|
51
|
+
export { RowActionMenu } from "./RowActionMenu";
|
|
52
|
+
export type { RowActionItem } from "./RowActionMenu";
|
|
52
53
|
|
|
53
|
-
export { PageHeader } from
|
|
54
|
-
export { SummaryTile } from
|
|
55
|
-
export type { SummaryTone } from
|
|
56
|
-
export { Facet, FacetSection, FacetButton } from
|
|
57
|
-
export type { FacetTone } from
|
|
58
|
-
export { SortableTh, TH_CLASS } from
|
|
59
|
-
export type { SortDir } from
|
|
60
|
-
export { DistributionBar, DistributionLegendChip } from
|
|
61
|
-
export type { DistributionSegment } from
|
|
62
|
-
export { SearchPillInput } from
|
|
63
|
-
export type { SearchModifier, SearchPillInputProps } from
|
|
64
|
-
export {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
54
|
+
export { PageHeader } from "./PageHeader";
|
|
55
|
+
export { SummaryTile } from "./SummaryTile";
|
|
56
|
+
export type { SummaryTone } from "./SummaryTile";
|
|
57
|
+
export { Facet, FacetSection, FacetButton } from "./Facet";
|
|
58
|
+
export type { FacetTone } from "./Facet";
|
|
59
|
+
export { SortableTh, TH_CLASS } from "./SortableTh";
|
|
60
|
+
export type { SortDir } from "./SortableTh";
|
|
61
|
+
export { DistributionBar, DistributionLegendChip } from "./DistributionBar";
|
|
62
|
+
export type { DistributionSegment } from "./DistributionBar";
|
|
63
|
+
export { SearchPillInput } from "./SearchPillInput";
|
|
64
|
+
export type { SearchModifier, SearchPillInputProps } from "./SearchPillInput";
|
|
65
|
+
export {
|
|
66
|
+
CardSection,
|
|
67
|
+
CardBody,
|
|
68
|
+
TerminalBlock,
|
|
69
|
+
TerminalBlockLabel,
|
|
70
|
+
renderProse,
|
|
71
|
+
NEUTRAL_CHIP_CLASS,
|
|
72
|
+
KIND_CHIP_CLASS,
|
|
73
|
+
} from "./CardSection";
|
|
74
|
+
export type { CardSectionTone } from "./CardSection";
|
|
75
|
+
export { MultiSelectPicker } from "./MultiSelectPicker";
|
|
76
|
+
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
|
-
<
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
package/src/theme/components.css
CHANGED
|
@@ -142,25 +142,6 @@
|
|
|
142
142
|
padding: 0.75rem;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
/* ── ISSUE ROW DETAILS ── */
|
|
146
|
-
|
|
147
|
-
.issue-details-motion {
|
|
148
|
-
display: grid;
|
|
149
|
-
grid-template-rows: 0fr;
|
|
150
|
-
overflow: hidden;
|
|
151
|
-
transition: grid-template-rows 200ms ease-out;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.issue-details-motion-open {
|
|
155
|
-
grid-template-rows: 1fr;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
@starting-style {
|
|
159
|
-
.issue-details-motion-open {
|
|
160
|
-
grid-template-rows: 0fr;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
145
|
/* ── SUBTLE BORDERS ── */
|
|
165
146
|
|
|
166
147
|
.table-divide-subtle > * + * {
|
package/src/utils/animation.ts
CHANGED
|
@@ -1,27 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Central animation presets for consistent motion across the UI.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
+
}
|