diff --git a/src/client/renderer.tsx b/src/client/renderer.tsx index 4738e35..239a270 100644 --- a/src/client/renderer.tsx +++ b/src/client/renderer.tsx @@ -92,12 +92,66 @@ export function DshCodeBlockNode({ node, ctx }: NodeComponentProps`, then the OS preference. + */ +function detectDshDark(): boolean { + try { + const value = getComputedStyle(document.body).getPropertyValue('--dsw-alias-bg-base').trim() + if (value !== '') { + const match = /(?:rgba?\(\s*)?(\d+)[,\s]+(\d+)[,\s]+(\d+)/.exec(value) + if (match !== null) { + return 0.2126 * Number(match[1]) + 0.7152 * Number(match[2]) + 0.0722 * Number(match[3]) < DARK_BG_LUMINANCE + } + } + } catch { /* document body not available yet */ } + try { + if (document.body.hasAttribute('data-ds-dark-theme')) return true + } catch { /* ignore */ } + try { + return window.matchMedia('(prefers-color-scheme: dark)').matches + } catch { + return false + } +} + +/** Reactive DSH dark-mode state; re-renders when the shell flips theme tokens or attributes. */ +export function useDshIsDark(): boolean { + const [dark, setDark] = useState(detectDshDark) + useEffect(() => { + let timer: ReturnType | undefined + const schedule = () => { + if (timer !== undefined) clearTimeout(timer) + timer = setTimeout(() => setDark(detectDshDark()), 80) + } + try { + const observer = new MutationObserver(schedule) + const options: MutationObserverInit = { attributes: true, attributeFilter: ['data-ds-dark-theme', 'style'] } + observer.observe(document.documentElement, options) + observer.observe(document.body, options) + return () => { + if (timer !== undefined) clearTimeout(timer) + observer.disconnect() + } + } catch { /* MutationObserver unavailable */ } + return undefined + }, []) + return dark +} + /** Markstream wrapper configured for untrusted assistant output. */ export const MarkstreamMarkdown = memo(function MarkstreamMarkdown({ text, streaming, fileMentions }: { text: string streaming: boolean fileMentions?: MarkdownFileMentions | undefined }) { + const isDark = useDshIsDark() const codeBlockProps = useMemo(() => ({ fileMentions: streaming ? undefined : fileMentions, }), [fileMentions, streaming]) @@ -106,6 +160,7 @@ export const MarkstreamMarkdown = memo(function MarkstreamMarkdown({ text, strea { expect(view.container.querySelector('.md-code-block')).toBeNull() plugin.dispose() }) + + it('follows the DSH shell dark theme and reacts to live switches', async () => { + const plugin = mountPlugin() + document.body.setAttribute('data-ds-dark-theme', '') + const view = render( + , + ) + await waitFor(() => { + expect(view.container.querySelector('.markstream-react.dark')).not.toBeNull() + }) + document.body.removeAttribute('data-ds-dark-theme') + await waitFor(() => { + expect(view.container.querySelector('.markstream-react.dark')).toBeNull() + }) + view.unmount() + plugin.dispose() + }) })