diff --git a/packages/k8s-ui/src/components/logs/LogCore.theme.test.tsx b/packages/k8s-ui/src/components/logs/LogCore.theme.test.tsx
new file mode 100644
index 0000000000..2affc30e14
--- /dev/null
+++ b/packages/k8s-ui/src/components/logs/LogCore.theme.test.tsx
@@ -0,0 +1,77 @@
+// @vitest-environment jsdom
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { LogCore } from './LogCore'
+
+Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
+
+let root: Root
+let element: HTMLDivElement
+beforeEach(() => {
+ localStorage.clear()
+ element = document.createElement('div')
+ document.body.appendChild(element)
+ root = createRoot(element)
+})
+afterEach(async () => {
+ await act(async () => root.unmount())
+ element.remove()
+})
+
+const noop = () => {}
+async function render(props: { forceDark?: boolean; defaultDark?: boolean }) {
+ await act(async () =>
+ root.render(
+ ,
+ ),
+ )
+}
+const toggle = () => element.querySelector('button[aria-label^="Switch log viewer"]')
+
+describe('LogCore theme', () => {
+ it('defaults to dark when no theme hint is given', async () => {
+ await render({})
+ expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to light mode')
+ })
+
+ it('follows defaultDark and keeps the toggle available', async () => {
+ await render({ defaultDark: false })
+ expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to dark mode')
+ await render({ defaultDark: true })
+ expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to light mode')
+ })
+
+ it('prefers the saved toggle choice over defaultDark', async () => {
+ localStorage.setItem('radar-logs-dark', 'true')
+ await render({ defaultDark: false })
+ expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to light mode')
+ })
+
+ it('returns to the saved choice when a forceDark pin is lifted', async () => {
+ localStorage.setItem('radar-logs-dark', 'false')
+ await render({ forceDark: true, defaultDark: false })
+ expect(toggle()).toBeNull()
+ await render({ defaultDark: false })
+ expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to dark mode')
+ })
+
+ it('ignores a saved value that is not a palette choice', async () => {
+ localStorage.setItem('radar-logs-dark', 'garbage')
+ await render({ defaultDark: false })
+ expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to dark mode')
+ })
+
+ it('hides the toggle when forceDark is set', async () => {
+ await render({ forceDark: true, defaultDark: false })
+ expect(toggle()).toBeNull()
+ })
+})
diff --git a/packages/k8s-ui/src/components/logs/LogCore.tsx b/packages/k8s-ui/src/components/logs/LogCore.tsx
index e1a6d2113f..ad7ce13efd 100644
--- a/packages/k8s-ui/src/components/logs/LogCore.tsx
+++ b/packages/k8s-ui/src/components/logs/LogCore.tsx
@@ -66,6 +66,12 @@ interface LogCoreProps {
* the viewer manages its own palette via localStorage and the Sun/Moon button.
*/
forceDark?: boolean
+ /**
+ * Palette to use when the user hasn't picked one with the Sun/Moon toggle
+ * (e.g. the host app's theme). Unlike `forceDark`, the toggle stays visible.
+ * Defaults to dark.
+ */
+ defaultDark?: boolean
}
interface LevelOption {
@@ -152,12 +158,14 @@ export function LogCore({
emptyCommand,
errorMessage,
forceDark,
+ defaultDark = true,
}: LogCoreProps) {
const virtuosoRef = useRef(null)
const [atBottom, setAtBottom] = useState(true)
const themeLocked = typeof forceDark === 'boolean'
// Seed isDark: forceDark prop wins; else localStorage['radar-logs-dark'];
- // else default dark. See log-palette.ts for why the viewer is palette-driven
+ // else defaultDark (the host app's theme, dark if not given). See
+ // log-palette.ts for why the viewer is palette-driven
// instead of theme-token-driven.
const [isDark, setIsDark] = useState(() => {
if (typeof forceDark === 'boolean') return forceDark
@@ -166,13 +174,26 @@ export function LogCore({
if (v === 'false') return false
if (v === 'true') return true
} catch {}
- return true
+ return defaultDark
})
useEffect(() => {
if (typeof forceDark === 'boolean') {
setIsDark(forceDark)
}
}, [forceDark])
+ // Re-resolve whenever the host's hint changes, including when a forceDark pin
+ // is lifted: the user's saved pick wins, otherwise the host default.
+ useEffect(() => {
+ if (typeof forceDark === 'boolean') return
+ try {
+ const v = localStorage.getItem('radar-logs-dark')
+ if (v === 'true' || v === 'false') {
+ setIsDark(v === 'true')
+ return
+ }
+ } catch {}
+ setIsDark(defaultDark)
+ }, [defaultDark, forceDark])
const palette = useMemo(() => getLogPalette(isDark), [isDark])
const toggleDark = useCallback(() => {
if (themeLocked) return
diff --git a/packages/k8s-ui/src/components/logs/LogsViewer.tsx b/packages/k8s-ui/src/components/logs/LogsViewer.tsx
index 6e0368ac5b..c6ac52fdcb 100644
--- a/packages/k8s-ui/src/components/logs/LogsViewer.tsx
+++ b/packages/k8s-ui/src/components/logs/LogsViewer.tsx
@@ -28,8 +28,10 @@ export interface LogsViewerProps {
createStream?: (params: Omit) => EventSource
/** Override the download mechanism (e.g. for desktop apps where blob URLs fail). */
overrideDownload?: (content: string, mime: string, filename: string) => void
- /** Force dark mode on the logs container (default: true) */
+ /** Pin the logs container to dark (true) or light (false) and hide the toggle */
forceDark?: boolean
+ /** Palette used until the user toggles it, e.g. the app theme (default: true) */
+ defaultDark?: boolean
/**
* Open the stream automatically on mount (and on container switch) instead of
* loading a static snapshot. The user can still Stop, and a manual Stop is not
@@ -47,6 +49,7 @@ export function LogsViewer({
createStream,
overrideDownload,
forceDark,
+ defaultDark,
autoStream = false,
}: LogsViewerProps) {
const [selectedContainer, setSelectedContainer] = useState(initialContainer || containers[0] || '')
@@ -190,6 +193,7 @@ export function LogsViewer({
onClear={clear}
toolbarExtra={renderToolbarExtra}
forceDark={forceDark}
+ defaultDark={defaultDark}
/>
)
}
diff --git a/packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx b/packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx
index 2f8fe643b9..44dce73e23 100644
--- a/packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx
+++ b/packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx
@@ -53,8 +53,10 @@ export interface WorkloadLogsViewerProps {
createStream?: (params: WorkloadLogsFetchParams) => EventSource
/** Override the download mechanism (e.g. for desktop apps where blob URLs fail). */
overrideDownload?: (content: string, mime: string, filename: string) => void
- /** Force dark mode on the logs container (default: true) */
+ /** Pin the logs container to dark (true) or light (false) and hide the toggle */
forceDark?: boolean
+ /** Palette used until the user toggles it, e.g. the app theme (default: true) */
+ defaultDark?: boolean
/**
* Open the stream automatically on mount (and on container switch) instead of
* loading a static snapshot. The user can still Stop, and a manual Stop is not
@@ -63,7 +65,7 @@ export interface WorkloadLogsViewerProps {
autoStream?: boolean
}
-export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownload, forceDark, autoStream = false }: WorkloadLogsViewerProps) {
+export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownload, forceDark, defaultDark, autoStream = false }: WorkloadLogsViewerProps) {
const [selectedContainer, setSelectedContainer] = useState('')
const [pods, setPods] = useState([])
const [selectedPods, setSelectedPods] = useState>(new Set())
@@ -400,6 +402,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
emptyCommand={emptyCommand}
errorMessage={entries.length === 0 ? fetchError || streamError : null}
forceDark={forceDark}
+ defaultDark={defaultDark}
/>
)
}
diff --git a/web/src/components/logs/LogsViewer.tsx b/web/src/components/logs/LogsViewer.tsx
index a7ecc290ed..82e2835bfe 100644
--- a/web/src/components/logs/LogsViewer.tsx
+++ b/web/src/components/logs/LogsViewer.tsx
@@ -43,7 +43,10 @@ export function LogsViewer({ namespace, podName, containers, initialContainer, a
fetchLogs={fetchLogs}
createStream={makeStream}
overrideDownload={desktopDownload}
+ // Light logs on a dark app are never wanted: dark theme pins the palette,
+ // light theme only sets where it starts and leaves the toggle available.
forceDark={theme === 'dark' ? true : undefined}
+ defaultDark={false}
autoStream={autoStream}
/>
)
diff --git a/web/src/components/logs/WorkloadLogsViewer.tsx b/web/src/components/logs/WorkloadLogsViewer.tsx
index 72bd2477f2..9ba745a031 100644
--- a/web/src/components/logs/WorkloadLogsViewer.tsx
+++ b/web/src/components/logs/WorkloadLogsViewer.tsx
@@ -42,7 +42,10 @@ export function WorkloadLogsViewer({ kind, namespace, name, autoStream = true, s
fetchAll={fetchAll}
createStream={snapshotOnly ? undefined : makeStream}
overrideDownload={desktopDownload}
+ // Light logs on a dark app are never wanted: dark theme pins the palette,
+ // light theme only sets where it starts and leaves the toggle available.
forceDark={theme === 'dark' ? true : undefined}
+ defaultDark={false}
autoStream={autoStream}
/>
)