@skyhook-io/radar-app 1.14.2 → 1.14.3
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 +2 -2
- package/src/api/client.ts +30 -0
- package/src/components/CloudConnectFlow.tsx +21 -7
- package/src/components/CloudFunnelButton.tsx +85 -8
- package/src/components/cloudConnectHandoff.test.ts +10 -1
- package/src/components/cloudConnectHandoff.ts +15 -1
- package/src/components/diagnose/AIMarkdown.tsx +87 -0
- package/src/components/diagnose/ActivityTurn.tsx +1244 -0
- package/src/components/diagnose/AgentControls.tsx +603 -0
- package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
- package/src/components/diagnose/AnalysisStory.tsx +13 -18
- package/src/components/diagnose/ApplyDialog.tsx +289 -0
- package/src/components/diagnose/AssessmentCard.tsx +1560 -0
- package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
- package/src/components/diagnose/EvidenceCard.tsx +611 -0
- package/src/components/diagnose/Home.test.tsx +1 -3
- package/src/components/diagnose/Home.tsx +2 -1
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
- package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
- package/src/components/diagnose/InvestigationView.tsx +11 -28
- package/src/components/diagnose/StoryExcerpt.tsx +118 -0
- package/src/components/diagnose/assessmentCopy.ts +169 -0
- package/src/components/diagnose/investigationCase.test.tsx +631 -2
- package/src/components/diagnose/investigationCase.ts +132 -24
- package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +660 -0
- package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
- package/src/components/diagnose/investigationEvidence/adapters/listings.ts +134 -0
- package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
- package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
- package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
- package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
- package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
- package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
- package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
- package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
- package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
- package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
- package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
- package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
- package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
- package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
- package/src/components/diagnose/investigationEvidence/index.ts +1 -1
- package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +11 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
- package/src/components/diagnose/investigationEvidence/types.ts +97 -2
- package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
- package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
- package/src/components/diagnose/parts.test.tsx +3 -1
- package/src/components/diagnose/parts.tsx +26 -3904
- package/src/components/diagnose/target.ts +9 -3
- package/src/components/diagnose/toolCallLabel.test.ts +47 -1
- package/src/components/diagnose/toolCallLabel.ts +41 -2
- package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
- package/src/components/resources/PodFilePreview.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +12 -8
- package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
- package/src/components/ui/Omnibar.tsx +33 -10
- package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
- package/src/components/ui/Disclosure.tsx +0 -47
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { resourceKey } from "@skyhook-io/k8s-ui";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
kindToPluralWithGroup,
|
|
5
|
+
knownKindForPluralWithGroup,
|
|
6
|
+
} from "../../utils/navigation";
|
|
4
7
|
|
|
5
8
|
export interface InvestigationTargetIdentity {
|
|
6
9
|
kind: string;
|
|
@@ -31,7 +34,10 @@ export function runTargetKey(
|
|
|
31
34
|
export function formatInvestigationTarget(
|
|
32
35
|
target: InvestigationTargetIdentity,
|
|
33
36
|
): string {
|
|
34
|
-
const canonicalKind =
|
|
35
|
-
|
|
37
|
+
const canonicalKind =
|
|
38
|
+
knownKindForPluralWithGroup(target.kind, target.group) ?? target.kind;
|
|
39
|
+
const kind = target.group
|
|
40
|
+
? `${canonicalKind}.${target.group}`
|
|
41
|
+
: canonicalKind;
|
|
36
42
|
return `${kind} ${target.namespace ? `${target.namespace}/` : ""}${target.name}`;
|
|
37
43
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { describeToolCall } from "./toolCallLabel";
|
|
2
|
+
import { describeToolCall, investigationRunningLabel } from "./toolCallLabel";
|
|
3
3
|
|
|
4
4
|
describe("describeToolCall", () => {
|
|
5
5
|
it("never invents a singular from a kind", () => {
|
|
@@ -49,3 +49,49 @@ describe("describeToolCall", () => {
|
|
|
49
49
|
expect(describeToolCall("some_new_tool", undefined)).toBe("Some New Tool");
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
describe("investigationRunningLabel", () => {
|
|
54
|
+
const read = {
|
|
55
|
+
tool: "get_resource",
|
|
56
|
+
status: "done",
|
|
57
|
+
summary: JSON.stringify({
|
|
58
|
+
kind: "deployment",
|
|
59
|
+
namespace: "shop",
|
|
60
|
+
name: "api",
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
it("names the call in flight, keeps a finished one briefly, then thinks", () => {
|
|
64
|
+
expect(
|
|
65
|
+
investigationRunningLabel({
|
|
66
|
+
lastTool: { ...read, status: "running" },
|
|
67
|
+
reads: 1,
|
|
68
|
+
quietFor: 20000,
|
|
69
|
+
elapsedSeconds: 9,
|
|
70
|
+
}),
|
|
71
|
+
).toEqual({
|
|
72
|
+
current: "Reading Deployment shop/api…",
|
|
73
|
+
meta: "1 read · 9 s",
|
|
74
|
+
});
|
|
75
|
+
expect(
|
|
76
|
+
investigationRunningLabel({
|
|
77
|
+
lastTool: read,
|
|
78
|
+
reads: 3,
|
|
79
|
+
quietFor: 7999,
|
|
80
|
+
elapsedSeconds: 30,
|
|
81
|
+
}).current,
|
|
82
|
+
).toBe("Reading Deployment shop/api…");
|
|
83
|
+
expect(
|
|
84
|
+
investigationRunningLabel({
|
|
85
|
+
lastTool: read,
|
|
86
|
+
reads: 3,
|
|
87
|
+
quietFor: 8000,
|
|
88
|
+
elapsedSeconds: 30,
|
|
89
|
+
}),
|
|
90
|
+
).toEqual({ current: "Thinking…", meta: "3 reads · 30 s" });
|
|
91
|
+
});
|
|
92
|
+
it("says Investigating with nothing read yet and omits a timer under two seconds", () => {
|
|
93
|
+
expect(
|
|
94
|
+
investigationRunningLabel({ reads: 0, quietFor: 0, elapsedSeconds: 1 }),
|
|
95
|
+
).toEqual({ current: "Investigating…", meta: "" });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { prettyTool } from "./parts";
|
|
2
|
-
|
|
3
1
|
// One readable sentence for a tool call in flight, from the call's arguments:
|
|
4
2
|
// "Reading logs for pod shop/api-7d4 / api" tells the reader what the agent
|
|
5
3
|
// is looking at; "Get Pod Logs" tells them which tool ran.
|
|
@@ -133,3 +131,44 @@ function pluralLabel(kind: string): string {
|
|
|
133
131
|
const base = kindLabel(kind) ?? kind;
|
|
134
132
|
return base.endsWith("s") ? base : `${base}s`;
|
|
135
133
|
}
|
|
134
|
+
|
|
135
|
+
export function prettyTool(tool: string): string {
|
|
136
|
+
return tool.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const THINKING_AFTER_MS = 8000;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The Assessment card's running row: the call in flight or just finished on
|
|
143
|
+
* the left, the read count and the timer on the right. A finished call keeps
|
|
144
|
+
* its name for a few quiet seconds, then the label admits the agent is
|
|
145
|
+
* thinking rather than showing a stale read.
|
|
146
|
+
*/
|
|
147
|
+
export function investigationRunningLabel({
|
|
148
|
+
lastTool,
|
|
149
|
+
reads,
|
|
150
|
+
quietFor,
|
|
151
|
+
elapsedSeconds,
|
|
152
|
+
}: {
|
|
153
|
+
lastTool?: { tool?: string; status?: string; summary?: string };
|
|
154
|
+
reads: number;
|
|
155
|
+
quietFor: number;
|
|
156
|
+
elapsedSeconds: number;
|
|
157
|
+
}): { current: string; meta: string } {
|
|
158
|
+
let current = "Investigating";
|
|
159
|
+
if (lastTool?.tool) {
|
|
160
|
+
current =
|
|
161
|
+
lastTool.status === "running" || quietFor < THINKING_AFTER_MS
|
|
162
|
+
? describeToolCall(lastTool.tool, lastTool.summary)
|
|
163
|
+
: "Thinking";
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
current: `${current}…`,
|
|
167
|
+
meta: [
|
|
168
|
+
reads > 0 ? `${reads} ${reads === 1 ? "read" : "reads"}` : undefined,
|
|
169
|
+
elapsedSeconds >= 2 ? `${elapsedSeconds} s` : undefined,
|
|
170
|
+
]
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.join(" · "),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -26,7 +26,7 @@ import { ValuesViewer } from './ValuesViewer'
|
|
|
26
26
|
import { OwnedResources } from './OwnedResources'
|
|
27
27
|
import { TrackChartSourceDialog } from './TrackChartSourceDialog'
|
|
28
28
|
import { Collapse, CollapseChevron } from '@skyhook-io/k8s-ui/components/ui/Collapse'
|
|
29
|
-
import { Disclosure } from '
|
|
29
|
+
import { Disclosure } from '@skyhook-io/k8s-ui/components/ui/Disclosure'
|
|
30
30
|
|
|
31
31
|
interface HelmReleaseDrawerProps {
|
|
32
32
|
release: SelectedHelmRelease
|
|
@@ -3,7 +3,7 @@ import { X, Copy, Check, Radio, Terminal, MessageSquare, Code2, Pin } from 'luci
|
|
|
3
3
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
4
4
|
import { MCP_TOOL_CATALOG } from './mcpToolCatalog'
|
|
5
5
|
import { Tooltip } from '../ui/Tooltip'
|
|
6
|
-
import { Disclosure } from '
|
|
6
|
+
import { Disclosure } from '@skyhook-io/k8s-ui/components/ui/Disclosure'
|
|
7
7
|
|
|
8
8
|
interface MCPSetupDialogProps {
|
|
9
9
|
open: boolean
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { useSearchParams } from "react-router-dom";
|
|
3
3
|
import { Loader2, Wifi, WifiOff } from "lucide-react";
|
|
4
|
-
import { Disclosure } from "
|
|
4
|
+
import { Disclosure } from "@skyhook-io/k8s-ui/components/ui/Disclosure";
|
|
5
5
|
import {
|
|
6
6
|
AreaChart,
|
|
7
7
|
SeriesLegend,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useLayoutEffect, useId, useRef } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import { ExternalLink, X } from 'lucide-react'
|
|
4
|
-
import { Disclosure } from '
|
|
4
|
+
import { Disclosure } from '@skyhook-io/k8s-ui/components/ui/Disclosure'
|
|
5
5
|
import type { WorkloadMetrics } from '../../api/workloadMetrics'
|
|
6
6
|
import type { PrometheusTimeRange } from '../../api/client'
|
|
7
7
|
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "../../api/workloadMetrics";
|
|
19
19
|
import { latestWorkloadValue, workloadPodValues } from "./workloadMetricValues";
|
|
20
20
|
import { WorkloadMetricsHelpDialog } from './WorkloadMetricsHelpDialog';
|
|
21
|
-
import { Disclosure } from '
|
|
21
|
+
import { Disclosure } from '@skyhook-io/k8s-ui/components/ui/Disclosure';
|
|
22
22
|
|
|
23
23
|
interface Props {
|
|
24
24
|
kind: string;
|
|
@@ -3,7 +3,7 @@ import Editor from '@monaco-editor/react'
|
|
|
3
3
|
import { AlertTriangle, Download, FileText, RotateCw } from 'lucide-react'
|
|
4
4
|
import { PaneLoader, ensureMonacoRuntime } from '@skyhook-io/k8s-ui'
|
|
5
5
|
import { formatBytes } from '../../utils/format'
|
|
6
|
-
import { Disclosure } from '
|
|
6
|
+
import { Disclosure } from '@skyhook-io/k8s-ui/components/ui/Disclosure'
|
|
7
7
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
8
8
|
|
|
9
9
|
// A curated inline viewer for text files inside a pod container, rendered in
|
|
@@ -23,13 +23,17 @@ export function CommandPalette({ onClose, isOpen = true, ...callbacks }: Command
|
|
|
23
23
|
// with the standalone omnibar via useCommandItems so the two never drift.
|
|
24
24
|
const items = useCommandItems(callbacks)
|
|
25
25
|
|
|
26
|
-
// Focus input on mount
|
|
26
|
+
// Focus the input once logically open. Not on mount: the presence hook
|
|
27
|
+
// mounts the palette two frames before `isOpen` flips, and the root is
|
|
28
|
+
// `inert` until then, so a mount-time focus would be refused.
|
|
27
29
|
useEffect(() => {
|
|
28
|
-
inputRef.current?.focus()
|
|
29
|
-
}, [])
|
|
30
|
+
if (isOpen) inputRef.current?.focus()
|
|
31
|
+
}, [isOpen])
|
|
30
32
|
|
|
31
|
-
// Close on Escape (capture phase to beat the shortcut system)
|
|
33
|
+
// Close on Escape (capture phase to beat the shortcut system) — only while
|
|
34
|
+
// logically open: the palette stays mounted through its exit.
|
|
32
35
|
useEffect(() => {
|
|
36
|
+
if (!isOpen) return
|
|
33
37
|
const handler = (e: KeyboardEvent) => {
|
|
34
38
|
if (e.key === 'Escape') {
|
|
35
39
|
e.preventDefault()
|
|
@@ -39,7 +43,7 @@ export function CommandPalette({ onClose, isOpen = true, ...callbacks }: Command
|
|
|
39
43
|
}
|
|
40
44
|
document.addEventListener('keydown', handler, true)
|
|
41
45
|
return () => document.removeEventListener('keydown', handler, true)
|
|
42
|
-
}, [onClose])
|
|
46
|
+
}, [isOpen, onClose])
|
|
43
47
|
|
|
44
48
|
// Filter and rank results
|
|
45
49
|
const filteredItems = useMemo(() => {
|
|
@@ -122,16 +126,16 @@ export function CommandPalette({ onClose, isOpen = true, ...callbacks }: Command
|
|
|
122
126
|
let flatIndex = 0
|
|
123
127
|
|
|
124
128
|
return (
|
|
125
|
-
<div className="fixed inset-0 z-[100] flex items-start justify-center pt-[15vh]">
|
|
129
|
+
<div className="fixed inset-0 z-[100] flex items-start justify-center pt-[15vh]" inert={!isOpen || undefined}>
|
|
126
130
|
{/* Backdrop */}
|
|
127
131
|
<div
|
|
128
132
|
className={clsx(
|
|
129
133
|
'absolute inset-0 bg-theme-base/60 backdrop-blur-sm',
|
|
130
134
|
TRANSITION_BACKDROP,
|
|
131
|
-
isOpen ? 'opacity-100' : 'opacity-0'
|
|
135
|
+
isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
|
132
136
|
)}
|
|
133
137
|
style={overlayTransitionStyle(isOpen, 'dialog')}
|
|
134
|
-
onClick={onClose}
|
|
138
|
+
onClick={isOpen ? onClose : undefined}
|
|
135
139
|
/>
|
|
136
140
|
|
|
137
141
|
{/* Panel */}
|
|
@@ -17,8 +17,10 @@ export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverla
|
|
|
17
17
|
const [copied, setCopied] = useState<'json' | 'formatted' | null>(null)
|
|
18
18
|
const [reportOpened, setReportOpened] = useState(false)
|
|
19
19
|
|
|
20
|
-
// Close on Escape (capture phase)
|
|
20
|
+
// Close on Escape (capture phase) — only while logically open: the overlay
|
|
21
|
+
// stays mounted through its exit.
|
|
21
22
|
useEffect(() => {
|
|
23
|
+
if (!isOpen) return
|
|
22
24
|
const handler = (e: KeyboardEvent) => {
|
|
23
25
|
if (e.key === 'Escape') {
|
|
24
26
|
e.preventDefault()
|
|
@@ -28,7 +30,7 @@ export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverla
|
|
|
28
30
|
}
|
|
29
31
|
document.addEventListener('keydown', handler, true)
|
|
30
32
|
return () => document.removeEventListener('keydown', handler, true)
|
|
31
|
-
}, [onClose])
|
|
33
|
+
}, [isOpen, onClose])
|
|
32
34
|
|
|
33
35
|
const copyToClipboard = useCallback(async (type: 'json' | 'formatted') => {
|
|
34
36
|
if (!data) return
|
|
@@ -64,16 +66,16 @@ export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverla
|
|
|
64
66
|
}, [data])
|
|
65
67
|
|
|
66
68
|
return (
|
|
67
|
-
<div className="fixed inset-0 z-[100] flex items-start justify-center pt-[8vh]">
|
|
69
|
+
<div className="fixed inset-0 z-[100] flex items-start justify-center pt-[8vh]" inert={!isOpen || undefined}>
|
|
68
70
|
{/* Backdrop */}
|
|
69
71
|
<div
|
|
70
72
|
className={clsx(
|
|
71
73
|
'absolute inset-0 bg-theme-base/60 backdrop-blur-sm',
|
|
72
74
|
TRANSITION_BACKDROP,
|
|
73
|
-
isOpen ? 'opacity-100' : 'opacity-0'
|
|
75
|
+
isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
|
74
76
|
)}
|
|
75
77
|
style={overlayTransitionStyle(isOpen, 'dialog')}
|
|
76
|
-
onClick={onClose}
|
|
78
|
+
onClick={isOpen ? onClose : undefined}
|
|
77
79
|
/>
|
|
78
80
|
|
|
79
81
|
{/* Panel */}
|
|
@@ -2,6 +2,8 @@ import { useState, useMemo, useRef, useEffect, useCallback, forwardRef, useImper
|
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import { Search, CornerDownLeft, Loader2, AlertTriangle } from 'lucide-react'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
|
+
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
6
|
+
import { TRANSITION_BACKDROP, TRANSITION_MENU, TW_EASE_UI, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
|
|
5
7
|
import { SearchPillInput, type SearchModifier } from '@skyhook-io/k8s-ui'
|
|
6
8
|
import { getResourceIcon } from '../../utils/resource-icons'
|
|
7
9
|
import type { SearchHit, SearchMatchedField } from '../../api/client'
|
|
@@ -386,8 +388,10 @@ export const Omnibar = forwardRef<OmnibarHandle, OmnibarProps>(function Omnibar(
|
|
|
386
388
|
}, [open])
|
|
387
389
|
|
|
388
390
|
// Track the input's position so the portaled panel stays anchored under it.
|
|
391
|
+
// The anchor is kept through the exit (clearing it at logical close would
|
|
392
|
+
// unmount the fading panel) and re-measured on the next open.
|
|
389
393
|
useEffect(() => {
|
|
390
|
-
if (!open)
|
|
394
|
+
if (!open) return
|
|
391
395
|
const update = () => {
|
|
392
396
|
const el = containerRef.current
|
|
393
397
|
if (!el) return
|
|
@@ -406,6 +410,15 @@ export const Omnibar = forwardRef<OmnibarHandle, OmnibarProps>(function Omnibar(
|
|
|
406
410
|
const totalMatched = searchData?.total_matched ?? 0
|
|
407
411
|
const hasNsPill = pills.some((p) => p.key === 'ns')
|
|
408
412
|
const dropdownOpen = open && !suggesting && (rows.length > 0 || searchActive)
|
|
413
|
+
// Presence: the scrim and the results panel are menu-kind overlays (140ms
|
|
414
|
+
// in / 100ms out) and stay mounted through their exit. What the panel was
|
|
415
|
+
// showing at logical close is what fades out — `dropdownOpen` drops with
|
|
416
|
+
// `open`, so the last open-frame value is retained for the exit.
|
|
417
|
+
const portalShown = open && (dropdownOpen || suggesting)
|
|
418
|
+
const portal = useAnimatedUnmount(portalShown, overlayExitMs('menu'))
|
|
419
|
+
const panelWasOpen = useRef(dropdownOpen)
|
|
420
|
+
if (open) panelWasOpen.current = dropdownOpen
|
|
421
|
+
const showPanel = open ? dropdownOpen : panelWasOpen.current
|
|
409
422
|
|
|
410
423
|
const clearNsPills = () => { setPills((prev) => prev.filter((p) => p.key !== 'ns')); inputRef.current?.focus() }
|
|
411
424
|
|
|
@@ -422,8 +435,8 @@ export const Omnibar = forwardRef<OmnibarHandle, OmnibarProps>(function Omnibar(
|
|
|
422
435
|
>
|
|
423
436
|
<SearchPillInput
|
|
424
437
|
className={hero
|
|
425
|
-
?
|
|
426
|
-
:
|
|
438
|
+
? `min-h-14 px-5 rounded-2xl bg-theme-surface border border-theme-border shadow-theme-sm transition-[color,background-color,border-color,box-shadow] duration-[140ms] ${TW_EASE_UI} focus-within:border-[var(--color-brand-500)] focus-within:shadow-[0_0_0_4px_color-mix(in_srgb,var(--color-brand-500)_15%,transparent)]`
|
|
439
|
+
: `min-h-8 px-2.5 rounded-md bg-theme-elevated border border-transparent focus-within:border-theme-border focus-within:bg-theme-surface transition-[color,background-color,border-color] duration-[140ms] ${TW_EASE_UI}`}
|
|
427
440
|
inputClassName={hero ? 'text-lg py-4' : undefined}
|
|
428
441
|
text={text}
|
|
429
442
|
pills={pills}
|
|
@@ -448,7 +461,7 @@ export const Omnibar = forwardRef<OmnibarHandle, OmnibarProps>(function Omnibar(
|
|
|
448
461
|
}
|
|
449
462
|
/>
|
|
450
463
|
|
|
451
|
-
{
|
|
464
|
+
{portal.shouldRender && anchor && createPortal(
|
|
452
465
|
<>
|
|
453
466
|
{/* Scrim — separates the dropdown from the page, consistently in both
|
|
454
467
|
modes. At z-[15] it sits BELOW the rail/top bar (z-20/30), so the
|
|
@@ -459,15 +472,25 @@ export const Omnibar = forwardRef<OmnibarHandle, OmnibarProps>(function Omnibar(
|
|
|
459
472
|
top-bar launcher covers from below the field (its box is already in
|
|
460
473
|
the z-20 chrome). Click closes. */}
|
|
461
474
|
<div
|
|
462
|
-
className=
|
|
463
|
-
|
|
464
|
-
|
|
475
|
+
className={clsx(
|
|
476
|
+
'fixed left-0 right-0 bottom-0 z-[15] bg-black/15 dark:bg-black/50 backdrop-blur-[3px]',
|
|
477
|
+
TRANSITION_BACKDROP,
|
|
478
|
+
portal.isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none',
|
|
479
|
+
)}
|
|
480
|
+
style={{ top: hero ? 0 : anchor.top, ...overlayTransitionStyle(portal.isOpen, 'menu') }}
|
|
481
|
+
onClick={open ? () => { setOpen(false); inputRef.current?.blur() } : undefined}
|
|
465
482
|
/>
|
|
466
|
-
{
|
|
483
|
+
{showPanel && (
|
|
467
484
|
<div
|
|
468
485
|
ref={panelRef}
|
|
469
|
-
|
|
470
|
-
|
|
486
|
+
inert={!open || undefined}
|
|
487
|
+
style={{ position: 'fixed', top: anchor.top + 8, left: anchor.centerX, transform: 'translateX(-50%)', width: hero ? Math.round(anchor.width) : 640, maxWidth: 'calc(100vw - 2rem)', ...overlayTransitionStyle(portal.isOpen, 'menu') }}
|
|
488
|
+
className={clsx(
|
|
489
|
+
'z-[121] dialog shadow-theme-lg ring-1 ring-black/5 dark:ring-white/10 overflow-hidden origin-top',
|
|
490
|
+
TRANSITION_MENU,
|
|
491
|
+
portal.isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]',
|
|
492
|
+
!open && 'pointer-events-none',
|
|
493
|
+
)}
|
|
471
494
|
>
|
|
472
495
|
<div ref={listRef} className="max-h-[60vh] overflow-y-auto py-1">
|
|
473
496
|
{recentRows.length > 0 && (
|
|
@@ -182,8 +182,11 @@ export function ShortcutHelpOverlay({ onClose, currentView, isOpen = true }: Sho
|
|
|
182
182
|
const shortcuts = useActiveShortcuts()
|
|
183
183
|
const overlayRef = useRef<HTMLDivElement>(null)
|
|
184
184
|
|
|
185
|
-
// Close on Escape
|
|
185
|
+
// Close on Escape — only while logically open: the overlay stays mounted
|
|
186
|
+
// through its exit and must not keep swallowing Escape (or blocking
|
|
187
|
+
// clicks) while it fades.
|
|
186
188
|
useEffect(() => {
|
|
189
|
+
if (!isOpen) return
|
|
187
190
|
const handler = (e: KeyboardEvent) => {
|
|
188
191
|
if (e.key === 'Escape') {
|
|
189
192
|
e.preventDefault()
|
|
@@ -194,7 +197,7 @@ export function ShortcutHelpOverlay({ onClose, currentView, isOpen = true }: Sho
|
|
|
194
197
|
// Use capture to intercept before the shortcut system
|
|
195
198
|
document.addEventListener('keydown', handler, true)
|
|
196
199
|
return () => document.removeEventListener('keydown', handler, true)
|
|
197
|
-
}, [onClose])
|
|
200
|
+
}, [isOpen, onClose])
|
|
198
201
|
|
|
199
202
|
// Group shortcuts by category, merging duplicates with same description
|
|
200
203
|
// (e.g., "Zoom in" registered for both + and = shows as one row)
|
|
@@ -224,16 +227,16 @@ export function ShortcutHelpOverlay({ onClose, currentView, isOpen = true }: Sho
|
|
|
224
227
|
const isEmpty = globalCategories.length === 0 && !hasViewSection && !hasContextSection
|
|
225
228
|
|
|
226
229
|
return (
|
|
227
|
-
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
|
230
|
+
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4" inert={!isOpen || undefined}>
|
|
228
231
|
{/* Backdrop */}
|
|
229
232
|
<div
|
|
230
233
|
className={clsx(
|
|
231
234
|
'absolute inset-0 bg-theme-base/70 backdrop-blur-sm',
|
|
232
235
|
TRANSITION_BACKDROP,
|
|
233
|
-
isOpen ? 'opacity-100' : 'opacity-0'
|
|
236
|
+
isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
|
234
237
|
)}
|
|
235
238
|
style={overlayTransitionStyle(isOpen, 'dialog')}
|
|
236
|
-
onClick={onClose}
|
|
239
|
+
onClick={isOpen ? onClose : undefined}
|
|
237
240
|
/>
|
|
238
241
|
|
|
239
242
|
{/* Panel */}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { useState, type ReactNode } from 'react'
|
|
2
|
-
import { clsx } from 'clsx'
|
|
3
|
-
import { Collapse, CollapseChevron, useDisclosure } from '@skyhook-io/k8s-ui/components/ui/Collapse'
|
|
4
|
-
|
|
5
|
-
// In-flow disclosure with the app-standard motion — the replacement for native
|
|
6
|
-
// <details>/<summary> on primary paths. Native details snaps open with no
|
|
7
|
-
// transition and its marker ignores the caret language; this pairs a button
|
|
8
|
-
// header (aria-expanded / aria-controls via useDisclosure) with CollapseChevron
|
|
9
|
-
// and an animated Collapse panel. Content stays mounted while closed, exactly
|
|
10
|
-
// like native details, so server-rendered markup and in-page search still see
|
|
11
|
-
// the collapsed copy.
|
|
12
|
-
export function Disclosure({
|
|
13
|
-
summary,
|
|
14
|
-
children,
|
|
15
|
-
className,
|
|
16
|
-
summaryClassName,
|
|
17
|
-
chevronClassName = 'h-3.5 w-3.5',
|
|
18
|
-
defaultOpen = false,
|
|
19
|
-
}: {
|
|
20
|
-
summary: ReactNode
|
|
21
|
-
children: ReactNode
|
|
22
|
-
/** Wrapper (what the old <details> carried). */
|
|
23
|
-
className?: string
|
|
24
|
-
/** Header button (what the old <summary> carried). */
|
|
25
|
-
summaryClassName?: string
|
|
26
|
-
chevronClassName?: string
|
|
27
|
-
defaultOpen?: boolean
|
|
28
|
-
}) {
|
|
29
|
-
const [open, setOpen] = useState(defaultOpen)
|
|
30
|
-
const disclosure = useDisclosure(open)
|
|
31
|
-
return (
|
|
32
|
-
<div className={className}>
|
|
33
|
-
<button
|
|
34
|
-
type="button"
|
|
35
|
-
{...disclosure.buttonProps}
|
|
36
|
-
onClick={() => setOpen((value) => !value)}
|
|
37
|
-
className={clsx('flex w-full cursor-pointer items-center gap-1.5 text-left', summaryClassName)}
|
|
38
|
-
>
|
|
39
|
-
<CollapseChevron open={open} className={chevronClassName} />
|
|
40
|
-
{summary}
|
|
41
|
-
</button>
|
|
42
|
-
<Collapse open={open} id={disclosure.panelId}>
|
|
43
|
-
{children}
|
|
44
|
-
</Collapse>
|
|
45
|
-
</div>
|
|
46
|
-
)
|
|
47
|
-
}
|