@skyhook-io/k8s-ui 1.5.2 → 1.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/k8s-ui",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/skyhook-io/radar",
@@ -1,17 +1,20 @@
1
1
  import { useState, useMemo } from 'react'
2
2
  import { ChevronRight, ChevronDown } from 'lucide-react'
3
- import { getLevelColor } from '../../utils/log-format'
4
3
  import type { LogLevel } from './useLogBuffer'
4
+ import { getLogPalette, getLogLevelColor } from './log-palette'
5
5
 
6
6
  interface JsonLogLineProps {
7
7
  content: string
8
8
  level: LogLevel
9
9
  wordWrap: boolean
10
+ /** Dark/light palette selector. Defaults to `true` for the dark-by-default viewer. */
11
+ isDark?: boolean
10
12
  }
11
13
 
12
- export function JsonLogLine({ content, level, wordWrap }: JsonLogLineProps) {
14
+ export function JsonLogLine({ content, level, wordWrap, isDark = true }: JsonLogLineProps) {
13
15
  const [expanded, setExpanded] = useState(false)
14
- const levelColor = getLevelColor(level)
16
+ const palette = useMemo(() => getLogPalette(isDark), [isDark])
17
+ const levelColor = getLogLevelColor(level, isDark)
15
18
 
16
19
  const parsed = useMemo(() => {
17
20
  try {
@@ -36,17 +39,17 @@ export function JsonLogLine({ content, level, wordWrap }: JsonLogLineProps) {
36
39
  <span className={levelColor}>
37
40
  <button
38
41
  onClick={() => setExpanded(!expanded)}
39
- className="inline-flex items-center gap-0.5 hover:bg-theme-surface/50 rounded px-0.5 -ml-0.5 align-middle"
42
+ className={`inline-flex items-center gap-0.5 ${palette.hoverSurface} rounded px-0.5 -ml-0.5 align-middle`}
40
43
  >
41
44
  {expanded
42
- ? <ChevronDown className="w-3 h-3 shrink-0 text-theme-text-tertiary" />
43
- : <ChevronRight className="w-3 h-3 shrink-0 text-theme-text-tertiary" />
45
+ ? <ChevronDown className={`w-3 h-3 shrink-0 ${palette.textTertiary}`} />
46
+ : <ChevronRight className={`w-3 h-3 shrink-0 ${palette.textTertiary}`} />
44
47
  }
45
48
  </button>
46
49
  {!expanded ? (
47
50
  <span className={wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}>
48
51
  {summary}
49
- <span className="text-theme-text-tertiary ml-1">{`{${fieldCount} fields}`}</span>
52
+ <span className={`${palette.textTertiary} ml-1`}>{`{${fieldCount} fields}`}</span>
50
53
  </span>
51
54
  ) : (
52
55
  <span className={`block ml-4 ${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}`}>
@@ -1,22 +1,30 @@
1
1
  import { useRef, useCallback, useState, useMemo, useEffect, type ReactNode } from 'react'
2
2
  import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
3
- import { Play, Square, Download, Search, X, Terminal, RotateCcw, ChevronUp, ChevronDown, ChevronRight, CaseSensitive, Regex, WrapText, Clock, Copy, Trash2, Filter, Braces, Palette, ListCollapse } from 'lucide-react'
3
+ import { Play, Square, Download, Search, X, Terminal, RotateCcw, ChevronUp, ChevronDown, ChevronRight, CaseSensitive, Regex, WrapText, Clock, Copy, Trash2, Filter, Braces, Palette, ListCollapse, Sun, Moon } from 'lucide-react'
4
4
  import type { LogEntry, LogLevel } from './useLogBuffer'
5
5
  import { useLogSearch } from './useLogSearch'
6
6
  import { StructuredLogLine } from './StructuredLogLine'
7
7
  import { Tooltip } from '../ui/Tooltip'
8
8
  import {
9
9
  formatLogTimestamp,
10
- getLevelColor,
11
10
  highlightSearchMatches,
12
11
  stripAnsi,
13
12
  ansiToHtml,
14
13
  type TimestampFormat,
15
14
  TIMESTAMP_FORMAT_LABELS,
16
15
  } from '../../utils/log-format'
16
+ import { getLogPalette, getLogLevelColor, type LogPalette } from './log-palette'
17
17
 
18
18
  export type DownloadFormat = 'txt' | 'json' | 'csv'
19
19
 
20
+ /**
21
+ * `toolbarExtra` may be a plain ReactNode, or a function that receives the
22
+ * current dark/light context so wrappers can produce palette-matched controls.
23
+ */
24
+ export type ToolbarExtraRenderer =
25
+ | ReactNode
26
+ | ((ctx: { isDark: boolean; palette: LogPalette }) => ReactNode)
27
+
20
28
  interface LogCoreProps {
21
29
  entries: LogEntry[]
22
30
  isLoading: boolean
@@ -26,21 +34,40 @@ interface LogCoreProps {
26
34
  onRefresh: () => void
27
35
  onDownload: (format: DownloadFormat) => void
28
36
  onClear?: () => void
29
- toolbarExtra?: ReactNode
37
+ toolbarExtra?: ToolbarExtraRenderer
30
38
  showPodName?: boolean
31
39
  emptyMessage?: string
32
40
  errorMessage?: string | null
33
- /** Optionally force dark styling for embedded consumers. */
41
+ /**
42
+ * Hard override for the viewer palette. When set, the viewer stays pinned to
43
+ * that mode and hides the in-viewer dark/light toggle. When undefined,
44
+ * the viewer manages its own palette via localStorage and the Sun/Moon button.
45
+ */
34
46
  forceDark?: boolean
35
47
  }
36
48
 
37
- const LEVEL_OPTIONS: { level: LogLevel; label: string; color: string; activeColor: string }[] = [
38
- { level: 'error', label: 'ERR', color: 'text-red-400', activeColor: 'bg-red-500/30 dark:bg-red-500/20 text-red-600 dark:text-red-400 border-red-500/60 dark:border-red-500/40' },
39
- { level: 'warn', label: 'WARN', color: 'text-yellow-400', activeColor: 'bg-yellow-400/30 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400 border-yellow-500/60 dark:border-yellow-500/40' },
40
- { level: 'info', label: 'INFO', color: 'text-blue-400', activeColor: 'bg-blue-500/25 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 border-blue-500/60 dark:border-blue-500/40' },
41
- { level: 'debug', label: 'DBG', color: 'text-theme-text-secondary', activeColor: 'bg-theme-surface text-theme-text-secondary border-theme-border-light' },
49
+ interface LevelOption {
50
+ level: LogLevel
51
+ label: string
52
+ }
53
+
54
+ const LEVEL_OPTIONS: LevelOption[] = [
55
+ { level: 'error', label: 'ERR' },
56
+ { level: 'warn', label: 'WARN' },
57
+ { level: 'info', label: 'INFO' },
58
+ { level: 'debug', label: 'DBG' },
42
59
  ]
43
60
 
61
+ function getLevelActiveColor(level: LogLevel, palette: LogPalette): string {
62
+ switch (level) {
63
+ case 'error': return palette.levelActiveError
64
+ case 'warn': return palette.levelActiveWarn
65
+ case 'info': return palette.levelActiveInfo
66
+ case 'debug': return palette.levelActiveDebug
67
+ default: return palette.levelActiveDebug
68
+ }
69
+ }
70
+
44
71
  const TIMESTAMP_FORMAT_ORDER: TimestampFormat[] = [
45
72
  'time-local', 'time-utc', 'iso-local', 'iso-utc', 'relative', 'epoch',
46
73
  ]
@@ -82,10 +109,37 @@ export function LogCore({
82
109
  showPodName = false,
83
110
  emptyMessage = 'No logs available',
84
111
  errorMessage,
85
- forceDark = false,
112
+ forceDark,
86
113
  }: LogCoreProps) {
87
114
  const virtuosoRef = useRef<VirtuosoHandle>(null)
88
115
  const [atBottom, setAtBottom] = useState(true)
116
+ const themeLocked = typeof forceDark === 'boolean'
117
+ // Seed isDark: forceDark prop wins; else localStorage['radar-logs-dark'];
118
+ // else default dark. See log-palette.ts for why the viewer is palette-driven
119
+ // instead of theme-token-driven.
120
+ const [isDark, setIsDark] = useState<boolean>(() => {
121
+ if (typeof forceDark === 'boolean') return forceDark
122
+ try {
123
+ const v = localStorage.getItem('radar-logs-dark')
124
+ if (v === 'false') return false
125
+ if (v === 'true') return true
126
+ } catch {}
127
+ return true
128
+ })
129
+ useEffect(() => {
130
+ if (typeof forceDark === 'boolean') {
131
+ setIsDark(forceDark)
132
+ }
133
+ }, [forceDark])
134
+ const palette = useMemo(() => getLogPalette(isDark), [isDark])
135
+ const toggleDark = useCallback(() => {
136
+ if (themeLocked) return
137
+ setIsDark(prev => {
138
+ const next = !prev
139
+ try { localStorage.setItem('radar-logs-dark', String(next)) } catch {}
140
+ return next
141
+ })
142
+ }, [themeLocked])
89
143
  const [wordWrap, setWordWrap] = useState(() => {
90
144
  try { return localStorage.getItem('radar-logs-wrap') !== 'false' } catch { return true }
91
145
  })
@@ -296,14 +350,32 @@ export function LogCore({
296
350
  })
297
351
  }, [groupedEntries.length])
298
352
 
353
+ const toolbarExtraNode = typeof toolbarExtra === 'function'
354
+ ? toolbarExtra({ isDark, palette })
355
+ : toolbarExtra
356
+
357
+ // Composite "inactive toolbar button" classes — static literals so
358
+ // Tailwind's class scanner picks them up. Two variants for secondary
359
+ // vs tertiary default text color.
360
+ const iconBtnInactive = isDark
361
+ ? 'p-1.5 rounded transition-colors text-slate-400 hover:text-slate-100 hover:bg-slate-800'
362
+ : 'p-1.5 rounded transition-colors text-slate-600 hover:text-slate-900 hover:bg-slate-200'
363
+ const iconBtnInactiveTertiary = isDark
364
+ ? 'p-1.5 rounded transition-colors text-slate-500 hover:text-slate-100 hover:bg-slate-800'
365
+ : 'p-1.5 rounded transition-colors text-slate-400 hover:text-slate-900 hover:bg-slate-200'
366
+ // "disabled → tertiary on hover" for inactive level filter chips.
367
+ const levelChipInactive = isDark
368
+ ? 'border-transparent text-slate-600 hover:text-slate-500'
369
+ : 'border-transparent text-slate-300 hover:text-slate-400'
370
+
299
371
  return (
300
372
  <div
301
- className={`flex flex-col h-full bg-theme-base${forceDark ? ' dark' : ''}`}
302
- style={{ colorScheme: forceDark ? 'dark' : undefined, fontFamily: "'SF Mono', 'Cascadia Code', 'Fira Code', Menlo, Consolas, 'DejaVu Sans Mono', monospace" }}
373
+ className={`flex flex-col h-full ${palette.containerBg}`}
374
+ style={{ colorScheme: isDark ? 'dark' : 'light', fontFamily: "'SF Mono', 'Cascadia Code', 'Fira Code', Menlo, Consolas, 'DejaVu Sans Mono', monospace" }}
303
375
  >
304
376
  {/* Toolbar */}
305
- <div className="flex items-center gap-2 px-3 py-2 border-b border-theme-border bg-theme-surface">
306
- {toolbarExtra}
377
+ <div className={`flex items-center gap-2 px-3 py-2 border-b ${palette.border} ${palette.toolbarBg}`}>
378
+ {toolbarExtraNode}
307
379
 
308
380
  {/* Stream / Stop toggle — only shown when streaming is supported */}
309
381
  {onStartStream && (
@@ -313,7 +385,7 @@ export function LogCore({
313
385
  className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded transition-colors ${
314
386
  isStreaming
315
387
  ? 'bg-green-600 text-white hover:bg-green-700'
316
- : 'bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover'
388
+ : `${palette.elevatedBg} ${palette.textSecondary} ${palette.hoverBg}`
317
389
  }`}
318
390
  >
319
391
  {isStreaming ? <Square className="w-3 h-3" /> : <Play className="w-3 h-3" />}
@@ -327,7 +399,7 @@ export function LogCore({
327
399
  <button
328
400
  onClick={onRefresh}
329
401
  disabled={isLoading || isStreaming}
330
- className="flex items-center gap-1.5 px-2 py-1.5 text-xs rounded bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover disabled:opacity-50 disabled:cursor-not-allowed"
402
+ className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded ${palette.elevatedBg} ${palette.textSecondary} ${palette.hoverBg} disabled:opacity-50 disabled:cursor-not-allowed`}
331
403
  >
332
404
  <RotateCcw className={`w-3 h-3 ${isLoading ? 'animate-spin' : ''}`} />
333
405
  </button>
@@ -344,8 +416,8 @@ export function LogCore({
344
416
  onClick={() => toggleLevel(opt.level)}
345
417
  className={`px-1.5 py-0.5 text-[10px] font-medium rounded border transition-colors ${
346
418
  active
347
- ? opt.activeColor
348
- : 'border-transparent text-theme-text-disabled hover:text-theme-text-tertiary'
419
+ ? getLevelActiveColor(opt.level, palette)
420
+ : levelChipInactive
349
421
  }`}
350
422
  >
351
423
  {opt.label}{count > 0 ? ` ${count}` : ''}
@@ -363,7 +435,7 @@ export function LogCore({
363
435
  <button
364
436
  onClick={() => setExpandAllStructured(prev => !prev)}
365
437
  className={`p-1.5 rounded transition-colors ${
366
- expandAllStructured ? 'btn-brand-toggle' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
438
+ expandAllStructured ? palette.toolbarActive : iconBtnInactive
367
439
  }`}
368
440
  >
369
441
  <Braces className="w-4 h-4" />
@@ -377,7 +449,7 @@ export function LogCore({
377
449
  <button
378
450
  onClick={toggleTimestamps}
379
451
  className={`p-1.5 rounded-l transition-colors ${
380
- showTimestamps ? 'btn-brand-toggle' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
452
+ showTimestamps ? palette.toolbarActive : iconBtnInactive
381
453
  }`}
382
454
  >
383
455
  <Clock className="w-4 h-4" />
@@ -388,7 +460,7 @@ export function LogCore({
388
460
  <button
389
461
  onClick={() => setShowTsMenu(prev => !prev)}
390
462
  className={`px-2 py-1.5 rounded-r text-[10px] font-medium transition-colors whitespace-nowrap ${
391
- showTimestamps ? 'btn-brand-toggle' : 'text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated'
463
+ showTimestamps ? palette.toolbarActive : iconBtnInactiveTertiary
392
464
  }`}
393
465
  aria-label="Pick timestamp format"
394
466
  >
@@ -399,20 +471,20 @@ export function LogCore({
399
471
  </button>
400
472
  </Tooltip>
401
473
  {showTsMenu && (
402
- <div className="absolute top-full right-0 mt-1 w-44 bg-theme-elevated border border-theme-border rounded-lg shadow-lg z-50">
403
- <div className="px-3 py-1.5 text-[10px] uppercase tracking-wide text-theme-text-tertiary border-b border-theme-border">
474
+ <div className={`absolute top-full right-0 mt-1 w-44 ${palette.menuBg} border ${palette.border} rounded-lg shadow-lg z-50`}>
475
+ <div className={`px-3 py-1.5 text-[10px] uppercase tracking-wide ${palette.textTertiary} border-b ${palette.border}`}>
404
476
  Timestamp format
405
477
  </div>
406
478
  {TIMESTAMP_FORMAT_ORDER.map(fmt => (
407
479
  <button
408
480
  key={fmt}
409
481
  onClick={() => pickTsFormat(fmt)}
410
- className={`w-full text-left px-3 py-1.5 text-xs hover:bg-theme-hover flex items-center justify-between ${
411
- tsFormat === fmt ? 'text-theme-text-primary' : 'text-theme-text-secondary'
482
+ className={`w-full text-left px-3 py-1.5 text-xs ${palette.hoverBg} flex items-center justify-between ${
483
+ tsFormat === fmt ? palette.textPrimary : palette.textSecondary
412
484
  }`}
413
485
  >
414
486
  <span>{TIMESTAMP_FORMAT_LABELS[fmt]}</span>
415
- {tsFormat === fmt && <span className="text-[10px] text-blue-400">✓</span>}
487
+ {tsFormat === fmt && <span className={`text-[10px] ${palette.textAccent}`}>✓</span>}
416
488
  </button>
417
489
  ))}
418
490
  </div>
@@ -425,7 +497,7 @@ export function LogCore({
425
497
  <button
426
498
  onClick={toggleCollapseStacks}
427
499
  className={`p-1.5 rounded transition-colors ${
428
- collapseStacks ? 'btn-brand-toggle' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
500
+ collapseStacks ? palette.toolbarActive : iconBtnInactive
429
501
  }`}
430
502
  >
431
503
  <ListCollapse className="w-4 h-4" />
@@ -437,7 +509,7 @@ export function LogCore({
437
509
  <button
438
510
  onClick={toggleAnsi}
439
511
  className={`p-1.5 rounded transition-colors ${
440
- ansiEnabled ? 'btn-brand-toggle' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
512
+ ansiEnabled ? palette.toolbarActive : iconBtnInactive
441
513
  }`}
442
514
  >
443
515
  <Palette className="w-4 h-4" />
@@ -449,7 +521,7 @@ export function LogCore({
449
521
  <button
450
522
  onClick={toggleWrap}
451
523
  className={`p-1.5 rounded transition-colors ${
452
- wordWrap ? 'btn-brand-toggle' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
524
+ wordWrap ? palette.toolbarActive : iconBtnInactive
453
525
  }`}
454
526
  >
455
527
  <WrapText className="w-4 h-4" />
@@ -461,30 +533,42 @@ export function LogCore({
461
533
  <button
462
534
  onClick={() => search.isOpen ? search.close() : search.open()}
463
535
  className={`p-1.5 rounded transition-colors ${
464
- search.isOpen ? 'btn-brand-toggle' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
536
+ search.isOpen ? palette.toolbarActive : iconBtnInactive
465
537
  }`}
466
538
  >
467
539
  <Search className="w-4 h-4" />
468
540
  </button>
469
541
  </Tooltip>
470
542
 
543
+ {!themeLocked && (
544
+ <Tooltip content={isDark ? 'Switch to light mode' : 'Switch to dark mode'} delay={TIP_DELAY} position="bottom">
545
+ <button
546
+ onClick={toggleDark}
547
+ className={iconBtnInactive}
548
+ aria-label={isDark ? 'Switch log viewer to light mode' : 'Switch log viewer to dark mode'}
549
+ >
550
+ {isDark ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
551
+ </button>
552
+ </Tooltip>
553
+ )}
554
+
471
555
  {/* Download */}
472
556
  <div className="relative flex items-center" ref={downloadMenuRef}>
473
557
  <Tooltip content="Download logs" delay={TIP_DELAY} position="bottom">
474
558
  <button
475
559
  onClick={() => setShowDownloadMenu(prev => !prev)}
476
- className="p-1.5 rounded text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated"
560
+ className={iconBtnInactive}
477
561
  >
478
562
  <Download className="w-4 h-4" />
479
563
  </button>
480
564
  </Tooltip>
481
565
  {showDownloadMenu && (
482
- <div className="absolute top-full right-0 mt-1 w-32 bg-theme-elevated border border-theme-border rounded-lg shadow-lg z-50">
566
+ <div className={`absolute top-full right-0 mt-1 w-32 ${palette.menuBg} border ${palette.border} rounded-lg shadow-lg z-50`}>
483
567
  {(['txt', 'json', 'csv'] as DownloadFormat[]).map(fmt => (
484
568
  <button
485
569
  key={fmt}
486
570
  onClick={() => { onDownload(fmt); setShowDownloadMenu(false) }}
487
- className="w-full text-left px-3 py-2 text-xs text-theme-text-primary hover:bg-theme-hover first:rounded-t-lg last:rounded-b-lg"
571
+ className={`w-full text-left px-3 py-2 text-xs ${palette.textPrimary} ${palette.hoverBg} first:rounded-t-lg last:rounded-b-lg`}
488
572
  >
489
573
  {fmt.toUpperCase()}
490
574
  </button>
@@ -498,7 +582,7 @@ export function LogCore({
498
582
  <Tooltip content="Clear logs" delay={TIP_DELAY} position="bottom">
499
583
  <button
500
584
  onClick={onClear}
501
- className="p-1.5 rounded text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated"
585
+ className={iconBtnInactive}
502
586
  >
503
587
  <Trash2 className="w-4 h-4" />
504
588
  </button>
@@ -508,8 +592,8 @@ export function LogCore({
508
592
 
509
593
  {/* Search bar */}
510
594
  {search.isOpen && (
511
- <div className="flex items-center gap-2 px-3 py-2 border-b border-theme-border bg-theme-surface/50">
512
- <Search className="w-4 h-4 text-theme-text-secondary shrink-0" />
595
+ <div className={`flex items-center gap-2 px-3 py-2 border-b ${palette.border} ${palette.toolbarBgMuted}`}>
596
+ <Search className={`w-4 h-4 ${palette.textSecondary} shrink-0`} />
513
597
  <input
514
598
  type="text"
515
599
  value={search.query}
@@ -526,7 +610,7 @@ export function LogCore({
526
610
  }
527
611
  }}
528
612
  placeholder="Search logs..."
529
- className="flex-1 bg-transparent text-theme-text-primary text-sm placeholder-theme-text-disabled focus:outline-none min-w-0"
613
+ className={`flex-1 bg-transparent ${palette.textPrimary} text-sm ${palette.placeholder} focus:outline-none min-w-0`}
530
614
  autoFocus
531
615
  />
532
616
 
@@ -535,7 +619,7 @@ export function LogCore({
535
619
  <button
536
620
  onClick={search.toggleRegex}
537
621
  className={`p-1 rounded transition-colors ${
538
- search.isRegex ? 'btn-brand-toggle' : 'text-theme-text-tertiary hover:text-theme-text-secondary'
622
+ search.isRegex ? palette.toolbarActive : `${palette.textTertiary} ${palette.hoverText}`
539
623
  }`}
540
624
  >
541
625
  <Regex className="w-3.5 h-3.5" />
@@ -547,7 +631,7 @@ export function LogCore({
547
631
  <button
548
632
  onClick={search.toggleCaseSensitive}
549
633
  className={`p-1 rounded transition-colors ${
550
- search.isCaseSensitive ? 'btn-brand-toggle' : 'text-theme-text-tertiary hover:text-theme-text-secondary'
634
+ search.isCaseSensitive ? palette.toolbarActive : `${palette.textTertiary} ${palette.hoverText}`
551
635
  }`}
552
636
  >
553
637
  <CaseSensitive className="w-3.5 h-3.5" />
@@ -559,7 +643,7 @@ export function LogCore({
559
643
  <button
560
644
  onClick={search.toggleFilterMode}
561
645
  className={`p-1 rounded transition-colors ${
562
- search.isFilterMode ? 'btn-brand-toggle' : 'text-theme-text-tertiary hover:text-theme-text-secondary'
646
+ search.isFilterMode ? palette.toolbarActive : `${palette.textTertiary} ${palette.hoverText}`
563
647
  }`}
564
648
  >
565
649
  <Filter className="w-3.5 h-3.5" />
@@ -568,7 +652,7 @@ export function LogCore({
568
652
 
569
653
  {search.query && (
570
654
  <>
571
- <span className={`text-xs whitespace-nowrap ${search.regexError ? 'text-red-400' : 'text-theme-text-tertiary'}`}>
655
+ <span className={`text-xs whitespace-nowrap ${search.regexError ? palette.textError : palette.textTertiary}`}>
572
656
  {search.regexError
573
657
  ? 'Invalid regex'
574
658
  : search.matchCount > 0
@@ -581,7 +665,7 @@ export function LogCore({
581
665
  <button
582
666
  onClick={search.goToPrev}
583
667
  disabled={search.matchCount === 0}
584
- className="p-1 rounded text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-30"
668
+ className={`p-1 rounded ${palette.textSecondary} ${palette.hoverText} disabled:opacity-30`}
585
669
  >
586
670
  <ChevronUp className="w-3.5 h-3.5" />
587
671
  </button>
@@ -590,7 +674,7 @@ export function LogCore({
590
674
  <button
591
675
  onClick={search.goToNext}
592
676
  disabled={search.matchCount === 0}
593
- className="p-1 rounded text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-30"
677
+ className={`p-1 rounded ${palette.textSecondary} ${palette.hoverText} disabled:opacity-30`}
594
678
  >
595
679
  <ChevronDown className="w-3.5 h-3.5" />
596
680
  </button>
@@ -598,7 +682,7 @@ export function LogCore({
598
682
 
599
683
  <button
600
684
  onClick={() => search.setQuery('')}
601
- className="p-1 rounded text-theme-text-secondary hover:text-theme-text-primary"
685
+ className={`p-1 rounded ${palette.textSecondary} ${palette.hoverText}`}
602
686
  >
603
687
  <X className="w-3 h-3" />
604
688
  </button>
@@ -609,19 +693,19 @@ export function LogCore({
609
693
 
610
694
  {/* Log content */}
611
695
  {isLoading && entries.length === 0 ? (
612
- <div className="flex-1 flex items-center justify-center text-theme-text-tertiary">
696
+ <div className={`flex-1 flex items-center justify-center ${palette.textTertiary}`}>
613
697
  <div className="flex items-center gap-2">
614
698
  <RotateCcw className="w-4 h-4 animate-spin" />
615
699
  <span>Loading logs...</span>
616
700
  </div>
617
701
  </div>
618
702
  ) : errorMessage ? (
619
- <div className="flex-1 flex flex-col items-center justify-center text-red-400 gap-2">
703
+ <div className={`flex-1 flex flex-col items-center justify-center gap-2 ${palette.textError}`}>
620
704
  <Terminal className="w-8 h-8" />
621
705
  <span>{errorMessage}</span>
622
706
  </div>
623
707
  ) : groupedEntries.length === 0 ? (
624
- <div className="flex-1 flex flex-col items-center justify-center text-theme-text-tertiary gap-2">
708
+ <div className={`flex-1 flex flex-col items-center justify-center gap-2 ${palette.textTertiary}`}>
625
709
  <Terminal className="w-8 h-8" />
626
710
  <span>{emptyMessage}</span>
627
711
  </div>
@@ -651,6 +735,8 @@ export function LogCore({
651
735
  onFilterValue={handleFilterValue}
652
736
  isStackExpanded={expandedStacks.has(group.head.id)}
653
737
  onToggleStack={toggleStackExpanded}
738
+ isDark={isDark}
739
+ palette={palette}
654
740
  />
655
741
  )}
656
742
  className="h-full font-mono text-xs"
@@ -668,20 +754,20 @@ export function LogCore({
668
754
  )}
669
755
 
670
756
  {/* Keyboard shortcut hints */}
671
- <div className="flex items-center gap-4 px-3 py-1 border-t border-theme-border bg-theme-surface text-[10px] text-theme-text-disabled">
672
- <Shortcut keys="Ctrl+F" label="Search" />
673
- <Shortcut keys="Enter" label="Next match" />
674
- <Shortcut keys="Shift+Enter" label="Prev match" />
675
- <Shortcut keys="Esc" label="Close search" />
757
+ <div className={`flex items-center gap-4 px-3 py-1 border-t ${palette.border} ${palette.toolbarBg} text-[10px] ${palette.textDisabled}`}>
758
+ <Shortcut keys="Ctrl+F" label="Search" palette={palette} />
759
+ <Shortcut keys="Enter" label="Next match" palette={palette} />
760
+ <Shortcut keys="Shift+Enter" label="Prev match" palette={palette} />
761
+ <Shortcut keys="Esc" label="Close search" palette={palette} />
676
762
  </div>
677
763
  </div>
678
764
  )
679
765
  }
680
766
 
681
- function Shortcut({ keys, label }: { keys: string; label: string }) {
767
+ function Shortcut({ keys, label, palette }: { keys: string; label: string; palette: LogPalette }) {
682
768
  return (
683
769
  <span className="flex items-center gap-1">
684
- <kbd className="px-1 py-px rounded bg-theme-elevated border border-theme-border-light font-mono">{keys}</kbd>
770
+ <kbd className={`px-1 py-px rounded ${palette.elevatedBg} border ${palette.borderLight} font-mono`}>{keys}</kbd>
685
771
  <span>{label}</span>
686
772
  </span>
687
773
  )
@@ -702,6 +788,8 @@ interface LogLineProps {
702
788
  onFilterValue?: (value: string) => void
703
789
  /** Optional lead element rendered at the start of the row (e.g. stack-trace toggle). */
704
790
  leadSlot?: ReactNode
791
+ isDark: boolean
792
+ palette: LogPalette
705
793
  }
706
794
 
707
795
  function LogLine({
@@ -718,8 +806,13 @@ function LogLine({
718
806
  defaultExpanded,
719
807
  onFilterValue,
720
808
  leadSlot,
809
+ isDark,
810
+ palette,
721
811
  }: LogLineProps) {
722
- const levelColor = getLevelColor(entry.level)
812
+ const levelColor = getLogLevelColor(entry.level, isDark)
813
+ const podTextColor = entry.podColorIndex !== undefined
814
+ ? palette.podColors[entry.podColorIndex % palette.podColors.length].text
815
+ : palette.textPrimary
723
816
 
724
817
  // Determine content rendering. Priority: search highlight > structured > ANSI/plain.
725
818
  let contentElement: React.ReactNode
@@ -741,6 +834,7 @@ function LogLine({
741
834
  isLogfmt={entry.isLogfmt}
742
835
  defaultExpanded={defaultExpanded}
743
836
  onFilterValue={onFilterValue}
837
+ isDark={isDark}
744
838
  />
745
839
  )
746
840
  } else if (ansiEnabled) {
@@ -765,11 +859,11 @@ function LogLine({
765
859
  }
766
860
 
767
861
  return (
768
- <div className={`flex hover:bg-theme-surface/50 group leading-5 px-2 ${isCurrentMatch ? 'bg-yellow-500/10' : ''}`}>
862
+ <div className={`flex ${palette.hoverSurface} group leading-5 px-2 ${isCurrentMatch ? palette.currentMatchBg : ''}`}>
769
863
  {leadSlot}
770
864
  {showTimestamp && entry.timestamp && (
771
865
  <span
772
- className="text-theme-text-tertiary select-none pr-2 whitespace-nowrap"
866
+ className={`${palette.textTertiary} select-none pr-2 whitespace-nowrap`}
773
867
  title={entry.timestamp}
774
868
  >
775
869
  {formatLogTimestamp(entry.timestamp, tsFormat)}
@@ -777,7 +871,7 @@ function LogLine({
777
871
  )}
778
872
  {showPodName && entry.pod && (
779
873
  <span
780
- className={`${entry.podColor || 'text-theme-text-primary'} select-none pr-2 whitespace-nowrap min-w-[80px] max-w-[120px] truncate`}
874
+ className={`${podTextColor} select-none pr-2 whitespace-nowrap min-w-[80px] max-w-[120px] truncate`}
781
875
  title={entry.pod}
782
876
  >
783
877
  [{entry.pod.split('-').slice(-2).join('-')}]
@@ -786,7 +880,7 @@ function LogLine({
786
880
  <span className="flex-1 min-w-0">{contentElement}</span>
787
881
  <button
788
882
  onClick={handleCopy}
789
- className="opacity-0 group-hover:opacity-100 ml-1 p-0.5 rounded text-theme-text-tertiary hover:text-theme-text-secondary shrink-0 transition-opacity"
883
+ className={`opacity-0 group-hover:opacity-100 ml-1 p-0.5 rounded ${palette.textTertiary} ${palette.hoverText} shrink-0 transition-opacity`}
790
884
  title="Copy line"
791
885
  >
792
886
  <Copy className="w-3 h-3" />
@@ -810,16 +904,18 @@ interface LogGroupItemProps {
810
904
  onFilterValue: (value: string) => void
811
905
  isStackExpanded: boolean
812
906
  onToggleStack: (id: number) => void
907
+ isDark: boolean
908
+ palette: LogPalette
813
909
  }
814
910
 
815
911
  function LogGroupItem(props: LogGroupItemProps) {
816
- const { group, isStackExpanded, onToggleStack, ...rest } = props
912
+ const { group, isStackExpanded, onToggleStack, palette, ...rest } = props
817
913
  const hasStack = group.continuations.length > 0
818
914
 
819
915
  const stackToggle = hasStack ? (
820
916
  <button
821
917
  onClick={() => onToggleStack(group.head.id)}
822
- className="mr-1 self-start p-0.5 rounded text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-surface/50 shrink-0"
918
+ className={`mr-1 self-start p-0.5 rounded ${palette.textTertiary} ${palette.hoverText} ${palette.hoverSurface} shrink-0`}
823
919
  title={isStackExpanded ? 'Collapse stack trace' : `Expand ${group.continuations.length} stack frames`}
824
920
  >
825
921
  {isStackExpanded
@@ -833,19 +929,20 @@ function LogGroupItem(props: LogGroupItemProps) {
833
929
  <LogLine
834
930
  entry={group.head}
835
931
  {...rest}
932
+ palette={palette}
836
933
  leadSlot={stackToggle}
837
934
  />
838
935
  {hasStack && !isStackExpanded && (
839
936
  <button
840
937
  onClick={() => onToggleStack(group.head.id)}
841
- className="block w-full text-left pl-6 pr-2 py-0 text-[10px] text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-surface/40"
938
+ className={`block w-full text-left pl-6 pr-2 py-0 text-[10px] ${palette.textTertiary} ${palette.hoverText} ${palette.hoverSurface}`}
842
939
  >
843
940
  [+{group.continuations.length} stack {group.continuations.length === 1 ? 'line' : 'lines'}]
844
941
  </button>
845
942
  )}
846
943
  {hasStack && isStackExpanded && group.continuations.map(cont => (
847
944
  <div key={cont.id} className="pl-4">
848
- <LogLine entry={cont} {...rest} isCurrentMatch={false} />
945
+ <LogLine entry={cont} {...rest} palette={palette} isCurrentMatch={false} />
849
946
  </div>
850
947
  ))}
851
948
  </div>