@skyhook-io/k8s-ui 1.8.9 → 1.8.11

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 (130) hide show
  1. package/package.json +5 -4
  2. package/src/assets/gitops/argocd.png +0 -0
  3. package/src/assets/gitops/flux.svg +7 -0
  4. package/src/components/applications/AppChips.tsx +10 -0
  5. package/src/components/applications/ApplicationDetail.test.tsx +569 -0
  6. package/src/components/applications/ApplicationDetail.tsx +2517 -450
  7. package/src/components/applications/ApplicationsList.tsx +4 -1
  8. package/src/components/applications/ApplicationsView.tsx +101 -18
  9. package/src/components/applications/applications-view.test.tsx +47 -0
  10. package/src/components/applications/index.ts +1 -1
  11. package/src/components/audit/AuditAlerts.tsx +14 -10
  12. package/src/components/checks/ChecksView.tsx +81 -51
  13. package/src/components/checks/severity.ts +39 -45
  14. package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
  15. package/src/components/compare/CompareResourcePicker.tsx +2 -2
  16. package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
  17. package/src/components/gitops/GitOpsTableView.tsx +14 -59
  18. package/src/components/gitops/ManagedResourcesList.tsx +8 -11
  19. package/src/components/gitops/SyncCountdown.tsx +26 -17
  20. package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
  21. package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
  22. package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
  23. package/src/components/gitops/insights/insights-helpers.ts +69 -1
  24. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
  25. package/src/components/issues/IssuesView.tsx +219 -143
  26. package/src/components/issues/ResourceIssuesSection.test.tsx +40 -0
  27. package/src/components/issues/ResourceIssuesSection.tsx +28 -128
  28. package/src/components/issues/index.ts +3 -2
  29. package/src/components/issues/issue-timing.ts +53 -0
  30. package/src/components/issues/issues.test.ts +146 -4
  31. package/src/components/issues/severity.ts +29 -32
  32. package/src/components/logs/LogCore.tsx +14 -4
  33. package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
  34. package/src/components/logs/useLogStream.ts +8 -1
  35. package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
  36. package/src/components/resources/ResourcesSidebar.test.tsx +107 -0
  37. package/src/components/resources/ResourcesSidebar.tsx +70 -52
  38. package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
  39. package/src/components/resources/ResourcesView.tsx +243 -53
  40. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  41. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
  42. package/src/components/resources/renderers/JobRenderer.tsx +3 -1
  43. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
  44. package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
  45. package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
  46. package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
  47. package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
  48. package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
  49. package/src/components/resources/renderers/PodRenderer.tsx +81 -70
  50. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
  51. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  52. package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
  53. package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
  54. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  55. package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
  56. package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
  57. package/src/components/resources/renderers/index.ts +1 -0
  58. package/src/components/resources/resource-utils-capi.ts +1 -1
  59. package/src/components/resources/resource-utils.ts +31 -0
  60. package/src/components/shared/DetailShell.tsx +2 -2
  61. package/src/components/shared/ResourceActionsBar.tsx +140 -108
  62. package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
  63. package/src/components/timeline/TimelineList.tsx +371 -354
  64. package/src/components/timeline/TimelineStrip.test.tsx +194 -0
  65. package/src/components/timeline/TimelineStrip.tsx +836 -0
  66. package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
  67. package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
  68. package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
  69. package/src/components/timeline/TimelineToolbar.tsx +759 -0
  70. package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
  71. package/src/components/timeline/index.ts +23 -0
  72. package/src/components/timeline/scrubber-math.test.ts +235 -0
  73. package/src/components/timeline/scrubber-math.ts +249 -0
  74. package/src/components/timeline/shared.test.tsx +219 -0
  75. package/src/components/timeline/shared.tsx +232 -447
  76. package/src/components/timeline/timeline-filters.test.ts +196 -0
  77. package/src/components/timeline/timeline-filters.ts +180 -0
  78. package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
  79. package/src/components/timeline/timeline-lane-sort.ts +77 -0
  80. package/src/components/timeline/timeline-live.test.ts +131 -0
  81. package/src/components/timeline/timeline-live.ts +77 -0
  82. package/src/components/topology/GroupNode.tsx +9 -2
  83. package/src/components/topology/K8sResourceNode.test.ts +11 -0
  84. package/src/components/topology/K8sResourceNode.tsx +387 -243
  85. package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
  86. package/src/components/topology/TopologyGraph.tsx +89 -16
  87. package/src/components/topology/TopologySearch.tsx +2 -2
  88. package/src/components/topology/layout.ts +10 -2
  89. package/src/components/topology/topology.css +0 -92
  90. package/src/components/ui/Badge.tsx +5 -0
  91. package/src/components/ui/BoardSkeleton.tsx +47 -0
  92. package/src/components/ui/CardSection.tsx +117 -0
  93. package/src/components/ui/CodeViewer.tsx +2 -2
  94. package/src/components/ui/Collapse.tsx +59 -0
  95. package/src/components/ui/ConfirmDialog.tsx +7 -3
  96. package/src/components/ui/DialogPortal.tsx +31 -9
  97. package/src/components/ui/Input.tsx +24 -0
  98. package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
  99. package/src/components/ui/MultiSelectPicker.tsx +167 -0
  100. package/src/components/ui/PaneLoader.tsx +10 -2
  101. package/src/components/ui/ResourceBar.tsx +68 -25
  102. package/src/components/ui/SearchBox.tsx +18 -4
  103. package/src/components/ui/SearchPillInput.tsx +2 -2
  104. package/src/components/ui/Toast.tsx +1 -1
  105. package/src/components/ui/drawer-components.tsx +39 -26
  106. package/src/components/ui/index.ts +6 -0
  107. package/src/components/ui/severity-tone.ts +63 -0
  108. package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
  109. package/src/components/workload/WorkloadView.tsx +2682 -472
  110. package/src/components/workload/index.ts +1 -1
  111. package/src/theme/variables.css +15 -1
  112. package/src/types/core.ts +57 -4
  113. package/src/utils/api-resources.test.ts +54 -0
  114. package/src/utils/api-resources.ts +68 -1
  115. package/src/utils/application-topology.test.ts +198 -0
  116. package/src/utils/application-topology.ts +261 -0
  117. package/src/utils/applications.test.ts +167 -1
  118. package/src/utils/applications.ts +1157 -393
  119. package/src/utils/index.ts +2 -0
  120. package/src/utils/navigation.test.ts +32 -1
  121. package/src/utils/navigation.ts +70 -0
  122. package/src/utils/resource-hierarchy.test.ts +990 -1
  123. package/src/utils/resource-hierarchy.ts +905 -267
  124. package/src/utils/resource-icons.ts +2 -0
  125. package/src/utils/topology-neighborhood.test.ts +202 -1
  126. package/src/utils/topology-neighborhood.ts +194 -13
  127. package/src/utils/workflow-execution.test.ts +556 -0
  128. package/src/utils/workflow-execution.ts +814 -0
  129. package/src/utils/workload-colors.ts +2 -3
  130. package/tsconfig.json +2 -2
@@ -1,6 +1,7 @@
1
- import { useRef } from 'react'
1
+ import { useRef, type RefObject } from 'react'
2
2
  import { Search, X } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
+ import { Input } from './Input'
4
5
  import { useRegisterShortcut, type ShortcutScope } from '../../hooks/useKeyboardShortcuts'
5
6
 
6
7
  /** The standard list-view search box: themed input with a `/`-to-focus
@@ -17,6 +18,9 @@ export function SearchBox({
17
18
  className,
18
19
  onEnter,
19
20
  onArrowDown,
21
+ inputRef: externalRef,
22
+ shortcutEnabled = true,
23
+ onBlur,
20
24
  }: {
21
25
  value: string
22
26
  onChange: (value: string) => void
@@ -31,8 +35,17 @@ export function SearchBox({
31
35
  onEnter?: () => void
32
36
  /** ArrowDown in the box — hand focus off to list keyboard navigation. */
33
37
  onArrowDown?: () => void
38
+ /** Externally-owned input ref — lets a collapsible wrapper focus the box
39
+ * after it mounts. Falls back to an internal ref when omitted. */
40
+ inputRef?: RefObject<HTMLInputElement | null>
41
+ /** Set false when a wrapper owns the `/` shortcut, so it isn't registered
42
+ * twice under the same id. */
43
+ shortcutEnabled?: boolean
44
+ /** Blur handler — a collapsible wrapper uses it to fold back when empty. */
45
+ onBlur?: () => void
34
46
  }) {
35
- const inputRef = useRef<HTMLInputElement>(null)
47
+ const internalRef = useRef<HTMLInputElement>(null)
48
+ const inputRef = externalRef ?? internalRef
36
49
 
37
50
  useRegisterShortcut({
38
51
  id: shortcutId,
@@ -40,18 +53,19 @@ export function SearchBox({
40
53
  description: 'Focus search',
41
54
  category: 'Search',
42
55
  scope,
56
+ enabled: shortcutEnabled,
43
57
  handler: () => inputRef.current?.focus(),
44
58
  })
45
59
 
46
60
  return (
47
61
  <div className={clsx('relative', className)}>
48
62
  <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-theme-text-tertiary" />
49
- <input
63
+ <Input
50
64
  ref={inputRef}
51
- type="text"
52
65
  value={value}
53
66
  placeholder={placeholder}
54
67
  onChange={(e) => onChange(e.target.value)}
68
+ onBlur={onBlur}
55
69
  onKeyDown={(e) => {
56
70
  if (e.key === 'Escape') {
57
71
  inputRef.current?.blur()
@@ -2,6 +2,7 @@ import { useRef, useState, useMemo, useEffect, useCallback } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import { X } from 'lucide-react'
4
4
  import { clsx } from 'clsx'
5
+ import { Input } from './Input'
5
6
 
6
7
  // A structured search input: free-text plus removable `key:value` modifier
7
8
  // PILLS (ns:, kind:, label:, …). Typing a complete modifier token and pressing
@@ -236,9 +237,8 @@ export function SearchPillInput({
236
237
  less
237
238
  </button>
238
239
  )}
239
- <input
240
+ <Input
240
241
  ref={inputRef}
241
- type="text"
242
242
  value={text}
243
243
  onChange={(e) => { setDismissed(false); onChange({ text: e.target.value, pills }) }}
244
244
  onKeyDown={handleKeyDown}
@@ -215,7 +215,7 @@ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }
215
215
  <button
216
216
  onClick={toast.onDetailClick}
217
217
  className={clsx(
218
- 'mt-1.5 block text-xs font-mono break-all text-left rounded px-1.5 py-1 -ml-1.5 transition-colors',
218
+ 'mt-1.5 block text-xs font-mono break-words text-left rounded px-1.5 py-1 -ml-1.5 transition-colors',
219
219
  isError
220
220
  ? 'text-red-300 hover:bg-red-900/50'
221
221
  : isSuccess
@@ -1,4 +1,4 @@
1
- import { useState, createContext, useContext } from 'react'
1
+ import { createContext, useContext, useState } from 'react'
2
2
  import { ChevronRight, 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'
@@ -532,9 +532,11 @@ function ContainerResourcesLine({ resources }: { resources: any }) {
532
532
  const effective = getEffectiveResources(resources)
533
533
  if (Object.keys(effective).length === 0) return null
534
534
  return (
535
- <div className="text-xs text-theme-text-tertiary mt-1" title="Effective requests (requests, falling back to limits)">
536
- Resources: {formatResources(effective)}
537
- </div>
535
+ <Tooltip content="Effective requests (requests, falling back to limits)" delay={150}>
536
+ <div className="text-xs text-theme-text-tertiary mt-1">
537
+ Resources: {formatResources(effective)}
538
+ </div>
539
+ </Tooltip>
538
540
  )
539
541
  }
540
542
 
@@ -551,11 +553,15 @@ export function PodTemplateSection({ template }: { template: any }) {
551
553
  {initContainers.map((c: any) => (
552
554
  <div key={c.name} className="card-inner text-sm border-l-2 border-yellow-500/40">
553
555
  <div className="font-medium text-theme-text-primary">{c.name}</div>
554
- <div className="text-xs text-theme-text-secondary truncate" title={c.image}>{c.image}</div>
556
+ <Tooltip content={c.image} delay={300} wrapperClassName="block min-w-0">
557
+ <div className="text-xs text-theme-text-secondary truncate">{c.image}</div>
558
+ </Tooltip>
555
559
  {(c.command || c.args) && (
556
- <div className="text-xs text-theme-text-tertiary font-mono mt-1 truncate" title={[...(c.command || []), ...(c.args || [])].join(' ')}>
557
- $ {[...(c.command || []), ...(c.args || [])].join(' ')}
558
- </div>
560
+ <Tooltip content={[...(c.command || []), ...(c.args || [])].join(' ')} delay={300} wrapperClassName="block min-w-0">
561
+ <div className="text-xs text-theme-text-tertiary font-mono mt-1 truncate">
562
+ $ {[...(c.command || []), ...(c.args || [])].join(' ')}
563
+ </div>
564
+ </Tooltip>
559
565
  )}
560
566
  <ContainerResourcesLine resources={c.resources} />
561
567
  </div>
@@ -566,7 +572,9 @@ export function PodTemplateSection({ template }: { template: any }) {
566
572
  {containers.map((c: any) => (
567
573
  <div key={c.name} className="card-inner text-sm">
568
574
  <div className="font-medium text-theme-text-primary">{c.name}</div>
569
- <div className="text-xs text-theme-text-secondary truncate" title={c.image}>{c.image}</div>
575
+ <Tooltip content={c.image} delay={300} wrapperClassName="block min-w-0">
576
+ <div className="text-xs text-theme-text-secondary truncate">{c.image}</div>
577
+ </Tooltip>
570
578
  {c.ports && (
571
579
  <div className="text-xs text-theme-text-tertiary mt-1">
572
580
  Ports: {c.ports.map((p: any) => `${p.name ? `${p.name}: ` : ''}${p.containerPort}/${p.protocol || 'TCP'}`).join(', ')}
@@ -786,6 +794,7 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
786
794
  (relationships.configRefs && relationships.configRefs.length > 0) ||
787
795
  (relationships.consumers && relationships.consumers.length > 0) ||
788
796
  (relationships.scalers && relationships.scalers.length > 0) ||
797
+ (relationships.storageRefs && relationships.storageRefs.length > 0) ||
789
798
  (relationships.pdbs && relationships.pdbs.length > 0) ||
790
799
  (relationships.networkPolicies && relationships.networkPolicies.length > 0) ||
791
800
  (relationships.resourceClaims && relationships.resourceClaims.length > 0) ||
@@ -829,6 +838,9 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
829
838
  {relationships.scalers && relationships.scalers.length > 0 && (
830
839
  <RelationshipGroup label="Autoscaler" refs={dedupeRefs(relationships.scalers)} onNavigate={onNavigate} />
831
840
  )}
841
+ {relationships.storageRefs && relationships.storageRefs.length > 0 && (
842
+ <RelationshipGroup label="Storage" refs={dedupeRefs(relationships.storageRefs)} onNavigate={onNavigate} />
843
+ )}
832
844
  {relationships.pdbs && relationships.pdbs.length > 0 && (
833
845
  <RelationshipGroup label="Disruption Budget" refs={dedupeRefs(relationships.pdbs)} onNavigate={onNavigate} />
834
846
  )}
@@ -905,30 +917,31 @@ export function ResourceRefBadge({ resourceRef, onClick, wrapAtSeparator }: Reso
905
917
  const layoutClass = wrapAtSeparator
906
918
  ? 'badge max-w-full min-w-0 flex-wrap items-center whitespace-normal text-left leading-tight'
907
919
  : 'badge'
920
+ const tooltip = `${resourceRef.kind}: ${resourceRef.namespace}/${resourceRef.name}`
908
921
 
909
922
  if (onClick) {
910
923
  return (
911
- <button
912
- onClick={() => onClick(resourceRef)}
913
- className={clsx(
914
- layoutClass,
915
- 'hover:brightness-[0.92] dark:hover:brightness-125 transition-[filter]',
916
- kindClass
917
- )}
918
- title={`${resourceRef.kind}: ${resourceRef.namespace}/${resourceRef.name}`}
919
- >
920
- {content}
921
- </button>
924
+ <Tooltip content={tooltip} delay={150}>
925
+ <button
926
+ onClick={() => onClick(resourceRef)}
927
+ className={clsx(
928
+ layoutClass,
929
+ 'hover:brightness-[0.92] dark:hover:brightness-125 transition-[filter]',
930
+ kindClass
931
+ )}
932
+ >
933
+ {content}
934
+ </button>
935
+ </Tooltip>
922
936
  )
923
937
  }
924
938
 
925
939
  return (
926
- <span
927
- className={clsx(layoutClass, kindClass)}
928
- title={`${resourceRef.kind}: ${resourceRef.namespace}/${resourceRef.name}`}
929
- >
930
- {content}
931
- </span>
940
+ <Tooltip content={tooltip} delay={300}>
941
+ <span className={clsx(layoutClass, kindClass)}>
942
+ {content}
943
+ </span>
944
+ </Tooltip>
932
945
  )
933
946
  }
934
947
 
@@ -10,6 +10,7 @@ export type { EmptyStateTone, EmptyStateVariant } from './EmptyState'
10
10
  export { RestrictedState } from './RestrictedState'
11
11
  export { FetchResult } from './FetchResult'
12
12
  export { SearchBox } from './SearchBox'
13
+ export { Input } from './Input'
13
14
  export { FilterPill } from './FilterPill'
14
15
  export type { FilterPillTone } from './FilterPill'
15
16
  export { StatusDot, mapHealthToTone } from './status-tone'
@@ -19,6 +20,7 @@ export { ConfirmDialog } from './ConfirmDialog'
19
20
  export { HealthRing } from './HealthRing'
20
21
  export { MetricsChart, MetricsSparkline } from './MetricsChart'
21
22
  export * from './drawer-components'
23
+ export { Collapse, CollapseChevron } from './Collapse'
22
24
  export { ResourceBar } from './ResourceBar'
23
25
  export { ForceDeleteConfirmDialog } from './ForceDeleteConfirmDialog'
24
26
  export { ToastProvider, useToast, showApiError, showApiSuccess } from './Toast'
@@ -38,3 +40,7 @@ export { DistributionBar, DistributionLegendChip } from './DistributionBar'
38
40
  export type { DistributionSegment } from './DistributionBar'
39
41
  export { SearchPillInput } from './SearchPillInput'
40
42
  export type { SearchModifier, SearchPillInputProps } from './SearchPillInput'
43
+ export { CardSection, CardBody, TerminalBlock, renderProse, NEUTRAL_CHIP_CLASS, KIND_CHIP_CLASS } from './CardSection'
44
+ export type { CardSectionTone } from './CardSection'
45
+ export { MultiSelectPicker } from './MultiSelectPicker'
46
+ export type { MultiSelectPickerProps } from './MultiSelectPicker'
@@ -0,0 +1,63 @@
1
+ // Shared severity color vocabulary. The Issue card (2-tier: critical/warning)
2
+ // and the Check card (4-tier: critical/high/medium/low) are different severity
3
+ // axes, but every tier resolves to one of four hues — so the color strings live
4
+ // here once and each card maps its own severity enum onto a tone. A palette
5
+ // change lands in one place instead of drifting between the two cards.
6
+ //
7
+ // Literal class strings (no template interpolation) so Tailwind's scanner sees
8
+ // every utility.
9
+
10
+ export type SeverityTone = 'red' | 'orange' | 'amber' | 'yellow' | 'slate'
11
+
12
+ // Text color — leading severity glyph + emphasis text. Yellow takes the darker
13
+ // -700 step in light mode: its low luminance is illegible at -600 on a light
14
+ // surface, where red/orange/amber still clear.
15
+ export const TONE_TEXT_CLASS: Record<SeverityTone, string> = {
16
+ red: 'text-red-600 dark:text-red-400',
17
+ orange: 'text-orange-600 dark:text-orange-400',
18
+ amber: 'text-amber-600 dark:text-amber-400',
19
+ yellow: 'text-yellow-700 dark:text-yellow-400',
20
+ slate: 'text-slate-500 dark:text-slate-400',
21
+ }
22
+
23
+ // Solid fill — dots + the proportional distribution-bar segments.
24
+ export const TONE_FILL_CLASS: Record<SeverityTone, string> = {
25
+ red: 'bg-red-500',
26
+ orange: 'bg-orange-500',
27
+ amber: 'bg-amber-500',
28
+ yellow: 'bg-yellow-500',
29
+ slate: 'bg-slate-400',
30
+ }
31
+
32
+ // Left accent rail on a collapsed queue row — the scan-down severity cue + a
33
+ // faint hover tint. The expanded row swaps this for the header band below.
34
+ export const TONE_RAIL_CLASS: Record<SeverityTone, string> = {
35
+ red: 'border-l-red-500 hover:bg-red-50/40 dark:hover:bg-red-950/20',
36
+ orange: 'border-l-orange-500 hover:bg-orange-50/40 dark:hover:bg-orange-950/20',
37
+ amber: 'border-l-amber-500 hover:bg-amber-50/30 dark:hover:bg-amber-950/15',
38
+ yellow: 'border-l-yellow-500 hover:bg-yellow-50/30 dark:hover:bg-yellow-950/15',
39
+ slate: 'border-l-slate-300 dark:border-l-slate-600 hover:bg-theme-hover/40',
40
+ }
41
+
42
+ // Solid severity pill — the loud focus signal on an expanded card's header band
43
+ // (collapsed cards keep the soft BADGE pill so a long queue stays calm).
44
+ // The warm mid-tones (orange/amber/yellow) take dark text: white on them sits
45
+ // below WCAG for the 12px pill text.
46
+ export const TONE_SOLID_CLASS: Record<SeverityTone, string> = {
47
+ red: 'bg-red-600 text-white',
48
+ orange: 'bg-orange-500 text-orange-950',
49
+ amber: 'bg-amber-400 text-amber-950',
50
+ yellow: 'bg-yellow-400 text-yellow-950',
51
+ slate: 'bg-slate-500 text-white dark:bg-slate-600',
52
+ }
53
+
54
+ // Severity-tinted header band, shown only when a card is expanded. The left
55
+ // rail keeps its color so the queue's scan-down severity rhythm doesn't break
56
+ // at the focused row; no hover variant, so it doesn't lighten on hover.
57
+ export const TONE_HEADER_BAND_CLASS: Record<SeverityTone, string> = {
58
+ red: 'border-l-red-500 bg-red-50 dark:bg-red-950/40',
59
+ orange: 'border-l-orange-500 bg-orange-50 dark:bg-orange-950/30',
60
+ amber: 'border-l-amber-500 bg-amber-50 dark:bg-amber-950/30',
61
+ yellow: 'border-l-yellow-500 bg-yellow-50 dark:bg-yellow-950/30',
62
+ slate: 'border-l-slate-300 dark:border-l-slate-600 bg-slate-50 dark:bg-slate-900/40',
63
+ }
@@ -32,6 +32,9 @@ interface ResourceDetailDrawerProps {
32
32
  headerHeight?: number
33
33
  /** Left offset to exclude (e.g. sidebar width) so expanded mode doesn't cover the sidebar (default: 0) */
34
34
  leftOffset?: number
35
+ /** Right inset in px to leave clear (e.g. a docked side panel) so the drawer sits
36
+ * beside it instead of under it. Shared "right-side surface" contract (default: 0). */
37
+ rightInset?: number
35
38
  /** Render the content inside the drawer */
36
39
  children: (props: {
37
40
  resource: SelectedResource
@@ -81,7 +84,7 @@ function usePrefersReducedMotion(): boolean {
81
84
  return reduced
82
85
  }
83
86
 
84
- export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab, isOpen = true, expanded, onCollapse, onExpand, canCollapseToDrawer = true, onNavigateToResource, headerHeight: headerHeightProp, leftOffset = 0, children }: ResourceDetailDrawerProps) {
87
+ export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab, isOpen = true, expanded, onCollapse, onExpand, canCollapseToDrawer = true, onNavigateToResource, headerHeight: headerHeightProp, leftOffset = 0, rightInset = 0, children }: ResourceDetailDrawerProps) {
85
88
  const [drawerWidth, setDrawerWidth] = useState(() => getDefaultWidth(resource.kind))
86
89
  const [isResizing, setIsResizing] = useState(false)
87
90
  const resizeStartX = useRef(0)
@@ -225,7 +228,7 @@ export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab
225
228
  // During the pre-mount frames (transitioning but not yet armed) hold the frame
226
229
  // at its START width so the width transition fires only once the incoming
227
230
  // layer has painted; otherwise it snaps to the target.
228
- const fullW = `calc(100% - ${leftOffset}px)`
231
+ const fullW = `calc(100% - ${leftOffset}px - ${rightInset}px)`
229
232
  const startWidth = settledExpanded.current ? fullW : drawerWidth
230
233
  const targetWidth = expanded ? fullW : drawerWidth
231
234
  const containerWidth = transitioning && !crossfadeArmed ? startWidth : targetWidth
@@ -274,6 +277,7 @@ export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab
274
277
  style={{
275
278
  width: containerWidth,
276
279
  top: headerHeight,
280
+ right: rightInset, // leave room for a docked side panel (AI), else flush right
277
281
  height: `calc(100% - ${headerHeight}px - ${dockInset}px)`,
278
282
  // Inline so the morph duration/easing is controlled precisely (and stays
279
283
  // in lockstep with the crossfade + JS window). Width only animates when