Skip to content

Commit

Permalink
feat: add listener for theme change
Browse files Browse the repository at this point in the history
  • Loading branch information
2214962083 committed Nov 24, 2024
1 parent 4888435 commit 9147a4e
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ export interface BaseToolbarProps {
messageRef: React.RefObject<HTMLElement | null>
scrollContentRef: React.RefObject<HTMLElement | null>
className?: string
children: React.ReactNode
bottomOffset?: number
buildChildren: (props: { isFloating: boolean }) => React.ReactNode
}

export const BaseToolbar: FC<BaseToolbarProps> = ({
messageRef,
scrollContentRef,
className,
children,
bottomOffset = 16
bottomOffset = 16,
buildChildren
}) => {
const [isFloating, setIsFloating] = useState(false)
const [floatingPosition, setFloatingPosition] = useState({
Expand Down Expand Up @@ -68,7 +68,7 @@ export const BaseToolbar: FC<BaseToolbarProps> = ({

const toolbarContent = (
<div className={cn('relative flex z-10 border rounded-lg', className)}>
{children}
{buildChildren({ isFloating })}
</div>
)

Expand Down
110 changes: 61 additions & 49 deletions src/webview/components/chat/messages/toolbars/message-toolbars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import type { Conversation } from '@shared/entities'
import { ButtonWithTooltip } from '@webview/components/button-with-tooltip'
import { AlertAction } from '@webview/components/ui/alert-action'
import type { ButtonProps } from '@webview/components/ui/button'

import { BaseToolbar, type BaseToolbarProps } from './base-toolbar'

Expand All @@ -32,56 +33,67 @@ export const MessageToolbar: FC<MessageToolbarProps> = ({
onRegenerate,
...props
}) => (
<BaseToolbar {...props}>
{/* copy */}
{onCopy && (
<ButtonWithTooltip
tooltip="Copy"
variant="ghost"
size="iconXs"
onClick={() => onCopy(conversation)}
>
<CopyIcon className="size-3" />
</ButtonWithTooltip>
)}
<BaseToolbar
{...props}
buildChildren={({ isFloating }) => {
const buttonProps: Partial<ButtonProps> = {
variant: 'ghost',
size: isFloating ? 'iconSm' : 'iconXs'
}

{/* edit */}
{onEdit && (
<ButtonWithTooltip
tooltip="Edit"
variant="ghost"
size="iconXs"
onClick={() => onEdit(conversation)}
>
<Pencil2Icon className="size-3" />
</ButtonWithTooltip>
)}
const iconClassName = isFloating ? 'size-4' : 'size-3'

{/* delete */}
{onDelete && (
<AlertAction
title="Delete Items"
description="Are you sure?"
variant="destructive"
confirmText="Delete"
onConfirm={() => onDelete(conversation)}
>
<ButtonWithTooltip tooltip="Delete" variant="ghost" size="iconXs">
<TrashIcon className="size-3" />
</ButtonWithTooltip>
</AlertAction>
)}
return (
<>
{/* copy */}
{onCopy && (
<ButtonWithTooltip
tooltip="Copy"
{...buttonProps}
onClick={() => onCopy(conversation)}
>
<CopyIcon className={iconClassName} />
</ButtonWithTooltip>
)}

{/* regenerate */}
{onRegenerate && (
<ButtonWithTooltip
tooltip="Regenerate"
variant="ghost"
size="iconXs"
onClick={() => onRegenerate(conversation)}
>
<ReloadIcon className="size-3" />
</ButtonWithTooltip>
)}
</BaseToolbar>
{/* edit */}
{onEdit && (
<ButtonWithTooltip
tooltip="Edit"
{...buttonProps}
onClick={() => onEdit(conversation)}
>
<Pencil2Icon className={iconClassName} />
</ButtonWithTooltip>
)}

{/* delete */}
{onDelete && (
<AlertAction
title="Delete Items"
description="Are you sure?"
variant="destructive"
confirmText="Delete"
onConfirm={() => onDelete(conversation)}
>
<ButtonWithTooltip tooltip="Delete" {...buttonProps}>
<TrashIcon className={iconClassName} />
</ButtonWithTooltip>
</AlertAction>
)}

{/* regenerate */}
{onRegenerate && (
<ButtonWithTooltip
tooltip="Regenerate"
{...buttonProps}
onClick={() => onRegenerate(conversation)}
>
<ReloadIcon className={iconClassName} />
</ButtonWithTooltip>
)}
</>
)
}}
/>
)
24 changes: 19 additions & 5 deletions src/webview/components/theme-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,32 @@ export const ThemeSync = () => {
}
}, THEME_CHECK_INTERVAL)

const observer = new MutationObserver(() => {
if (themeLoaded) syncTheme()
const styleObserver = new MutationObserver(mutations => {
for (const mutation of mutations) {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'style' &&
themeLoaded
) {
const style = getComputedStyle(document.documentElement)
const newBackground = style.getPropertyValue(
'--vscode-sideBarTitle-background'
)
if (newBackground) {
syncTheme()
}
}
}
})

observer.observe(document.documentElement, {
styleObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
attributeFilter: ['style']
})

return () => {
clearInterval(intervalId)
observer.disconnect()
styleObserver.disconnect()
}
}, [themeLoaded])

Expand Down
1 change: 1 addition & 0 deletions src/webview/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const buttonVariants = cva(
xs: 'h-6 px-2 py-1 text-xs rounded-sm',
iconXs: 'size-6 py-1 text-xs rounded-sm',
sm: 'h-8 rounded-md px-3 text-xs',
iconSm: 'size-8 py-2 text-xs rounded-md',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9'
}
Expand Down
58 changes: 58 additions & 0 deletions src/webview/contexts/global-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, {
createContext,
FC,
useContext,
useEffect,
useState
} from 'react'
import { getLuminance } from 'color2k'

type GlobalContextValue = {
isApiInit: boolean
isDarkTheme: boolean
}

const GlobalContext = createContext<GlobalContextValue | null>(null)

export const useGlobalContext = () => {
const context = useContext(GlobalContext)
if (!context) {
throw new Error(
'useGlobalContext must be used within a GlobalContextProvider'
)
}
return context
}

export const GlobalContextProvider: FC<
Omit<GlobalContextValue, 'isDarkTheme'> & { children: React.ReactNode }
> = ({ isApiInit, children }) => {
const [isDarkTheme, setIsDarkTheme] = useState(true)

useEffect(() => {
const updateTheme = () => {
const backgroundColor = getComputedStyle(document.documentElement)
.getPropertyValue('--vscode-sideBarTitle-background')
.trim()
setIsDarkTheme(getLuminance(backgroundColor) < 0.5)
}

// Initial theme check
updateTheme()

// Watch for theme changes
const observer = new MutationObserver(updateTheme)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style']
})

return () => observer.disconnect()
}, [])

return (
<GlobalContext.Provider value={{ isApiInit, isDarkTheme }}>
{children}
</GlobalContext.Provider>
)
}
6 changes: 4 additions & 2 deletions src/webview/hooks/use-shiki-highlighter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'
import { useGlobalContext } from '@webview/contexts/global-context'
import { logger } from '@webview/utils/logger'
import { codeToHtml } from 'shiki'

Expand All @@ -9,6 +10,7 @@ export interface UseShikiHighlighterProps {

export const useShikiHighlighter = (props: UseShikiHighlighterProps) => {
const { code, language } = props
const { isDarkTheme } = useGlobalContext()
const [highlightedCode, setHighlightedCode] = useState<string>('')
const [isLoading, setIsLoading] = useState(true)

Expand All @@ -17,7 +19,7 @@ export const useShikiHighlighter = (props: UseShikiHighlighterProps) => {
try {
const html = await codeToHtml(code, {
lang: language,
theme: 'dark-plus'
theme: isDarkTheme ? 'dark-plus' : 'light-plus'
})
setHighlightedCode(html)
} catch (error) {
Expand All @@ -28,7 +30,7 @@ export const useShikiHighlighter = (props: UseShikiHighlighterProps) => {
}

highlightCode()
}, [code, language])
}, [code, language, isDarkTheme])

return { highlightedCode, isLoading }
}
16 changes: 11 additions & 5 deletions src/webview/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import './styles/global.css'

import { ThemeSync } from './components/theme-sync'
import { SparklesText } from './components/ui/sparkles-text'
import { GlobalContextProvider } from './contexts/global-context'
import { getSocketPort } from './services/api-client/get-socket-port'

const root = ReactDOM.createRoot(document.getElementById('app')!)

const AppWrapper = () => {
const [isLoading, setIsLoading] = useState(true)
const [isApiInit, setIsApiInit] = useState(false)
const [App, setApp] = useState<React.ComponentType | null>(null)

useEffect(() => {
Expand All @@ -23,7 +25,9 @@ const AppWrapper = () => {
}

const { default: AppComponent } = await import('./App')
const { api } = await import('./services/api-client')
const { api, initApi } = await import('./services/api-client')
initApi()
setIsApiInit(true)
window.isWin = await api.system.isWindows({})

setApp(() => AppComponent)
Expand All @@ -42,15 +46,17 @@ const AppWrapper = () => {
}

return (
<HashRouter>
<App />
</HashRouter>
<GlobalContextProvider isApiInit={isApiInit}>
<HashRouter>
<ThemeSync />
<App />
</HashRouter>
</GlobalContextProvider>
)
}

root.render(
<React.StrictMode>
<ThemeSync />
<AppWrapper />
</React.StrictMode>
)
10 changes: 7 additions & 3 deletions src/webview/services/api-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class APIClient {

private pendingRequests: Map<number, PendingRequest> = new Map()

constructor() {
init() {
const port = window.vscodeWebviewState?.socketPort

if (!port) throw new Error('Socket port not found in VSCode state')
Expand Down Expand Up @@ -102,7 +102,7 @@ export class APIClient {

export const createWebviewApi = <T extends readonly ControllerClass[]>() => {
const apiClient = new APIClient()
return new Proxy({} as APIType<T>, {
const api = new Proxy({} as APIType<T>, {
get: (target, controllerName: string) =>
new Proxy(
{},
Expand All @@ -118,6 +118,10 @@ export const createWebviewApi = <T extends readonly ControllerClass[]>() => {
}
)
}) as APIType<T>

const initApi = apiClient.init.bind(apiClient)

return { api, initApi }
}

export const api = createWebviewApi<Controllers>()
export const { api, initApi } = createWebviewApi<Controllers>()

0 comments on commit 9147a4e

Please sign in to comment.