add responsive dark mode

This commit is contained in:
Andrew Pareles 2024-12-16 18:57:28 -08:00
parent c485d2c478
commit c44497510b
9 changed files with 77 additions and 41 deletions

View file

@ -177,11 +177,6 @@ export type SettingName = UnionOfKeys<SettingsOfProvider[ProviderName]>
type DisplayInfo = {
title: string,
type: string,
placeholder: string,
}
export const titleOfProviderName = (providerName: ProviderName) => {
if (providerName === 'anthropic')
@ -202,11 +197,14 @@ export const titleOfProviderName = (providerName: ProviderName) => {
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
}
type DisplayInfo = {
title: string,
placeholder: string,
}
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
if (settingName === 'apiKey') {
return {
title: 'API Key',
type: 'string',
placeholder: providerName === 'anthropic' ? 'sk-ant-key...' : // sk-ant-api03-key
providerName === 'openAI' ? 'sk-proj-key...' :
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
@ -221,7 +219,6 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
title: providerName === 'ollama' ? 'Your Ollama endpoint' :
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
: '(never)',
type: 'string',
placeholder: providerName === 'ollama' ? voidProviderDefaults.ollama.endpoint
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
: '(never)',
@ -229,15 +226,13 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
}
else if (settingName === 'enabled') {
return {
title: 'Enabled?',
type: 'boolean',
title: '(never)',
placeholder: '(never)',
}
}
else if (settingName === 'models') {
return {
title: 'Available Models',
type: '(never)',
title: '(never)',
placeholder: '(never)',
}
}

View file

@ -8,7 +8,7 @@ import { mountFnGenerator } from '../util/mountFnGenerator.js'
// import { SidebarSettings } from './SidebarSettings.js';
import { useSidebarState } from '../util/services.js';
import { useIsDark, useSidebarState } from '../util/services.js';
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
// import { SidebarChat } from './SidebarChat.js';
@ -17,12 +17,12 @@ import { SidebarThreadSelector } from './SidebarThreadSelector.js';
import { SidebarChat } from './SidebarChat.js';
import ErrorBoundary from './ErrorBoundary.js';
export const Sidebar = () => {
export const Sidebar = ({ className }: { className: string }) => {
const sidebarState = useSidebarState()
const { isHistoryOpen, currentTab: tab } = sidebarState
// className='@@void-scope'
return <div className='@@void-scope' style={{ width: '100%', height: '100%' }}>
const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ width: '100%', height: '100%' }}>
<div className={`flex flex-col px-2 py-2 w-full h-full`}>
{/* <span onClick={() => {

View file

@ -21,9 +21,8 @@ import { ErrorDisplay } from './ErrorDisplay.js';
import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { getCmdKey } from '../../../helpers/getCmdKey.js'
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { VoidInputBox, VoidScrollableElt } from '../util/inputs.js';
import { VoidInputBox } from '../util/inputs.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { ScrollbarVisibility } from '../../../../../../../base/common/scrollable.js';
const IconX = ({ size, className = '' }: { size: number, className?: string }) => {

View file

@ -3,7 +3,7 @@
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useService } from './services.js';
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
@ -145,21 +145,32 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
};
export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => {
// export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => {
// const instanceRef = useRef<DomScrollableElement | null>(null);
// const [childrenPortal, setChildrenPortal] = useState<React.ReactNode | null>(null)
return <WidgetComponent
ctor={DomScrollableElement}
propsFn={useCallback((container) => {
return [container, options] as const;
}, [options])}
onCreateInstance={useCallback(() => { return [] }, [])}
dispose={useCallback((instance: DomScrollableElement) => {
console.log('calling dispose!!!!')
// instance.dispose();
// instance.getDomNode().remove()
}, [])}
>abcdefg</WidgetComponent>
}
// return <>
// <WidgetComponent
// ctor={DomScrollableElement}
// propsFn={useCallback((container) => {
// return [container, options] as const;
// }, [options])}
// onCreateInstance={useCallback((instance: DomScrollableElement) => {
// instanceRef.current = instance;
// setChildrenPortal(createPortal(children, instance.getDomNode()))
// return []
// }, [setChildrenPortal, children])}
// dispose={useCallback((instance: DomScrollableElement) => {
// console.log('calling dispose!!!!')
// // instance.dispose();
// // instance.getDomNode().remove()
// }, [])}
// >{children}</WidgetComponent>
// {childrenPortal}
// </>
// }
// export const VoidSelectBox = <T,>({ onChangeSelection, initVal, selectBoxRef, options }: {
// initVal: T;

View file

@ -9,7 +9,7 @@ import { _registerServices } from './services.js';
import { ReactServicesType } from '../../../helpers/reactServicesHelper.js';
export const mountFnGenerator = (Component: React.FC) => (rootElement: HTMLElement, services: ReactServicesType) => {
export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => (rootElement: HTMLElement, services: ReactServicesType) => {
if (typeof document === 'undefined') {
console.error('index.tsx error: document was undefined')
return
@ -17,8 +17,9 @@ export const mountFnGenerator = (Component: React.FC) => (rootElement: HTMLEleme
const disposables = _registerServices(services)
const root = ReactDOM.createRoot(rootElement)
root.render(<Component />);
root.render(<Component />); // tailwind dark theme indicator
return disposables
}

View file

@ -11,6 +11,7 @@ import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { ReactServicesType } from '../../../helpers/reactServicesHelper.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
@ -31,7 +32,8 @@ const settingsStateListeners: Set<(s: VoidSettingsState) => void> = new Set()
let refreshModelState: RefreshModelState
const refreshModelStateListeners: Set<(s: RefreshModelState) => void> = new Set()
let colorThemeState: ColorScheme
const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set()
// must call this before you can use any of the hooks below
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
@ -48,7 +50,7 @@ export const _registerServices = (services_: ReactServicesType) => {
wasCalled = true
services = services_
const { sidebarStateService, settingsStateService, threadsStateService, refreshModelService } = services
const { sidebarStateService, settingsStateService, threadsStateService, refreshModelService, themeService } = services
sidebarState = sidebarStateService.state
disposables.push(
@ -82,6 +84,14 @@ export const _registerServices = (services_: ReactServicesType) => {
})
)
colorThemeState = themeService.getColorTheme().type
disposables.push(
themeService.onDidColorThemeChange(theme => {
colorThemeState = theme.type
colorThemeStateListeners.forEach(l => l(colorThemeState))
})
)
return disposables
}
@ -139,3 +149,18 @@ export const useRefreshModelState = () => {
export const useIsDark = () => {
const [s, ss] = useState(colorThemeState)
useEffect(() => {
ss(colorThemeState)
colorThemeStateListeners.add(ss)
return () => { colorThemeStateListeners.delete(ss) }
}, [ss])
// s is the theme, return isDark instead of s
const isDark = s === ColorScheme.DARK || s === ColorScheme.HIGH_CONTRAST_DARK
return isDark
}

View file

@ -3,7 +3,7 @@ import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox
import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, ModelInfo } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidInputBox } from '../util/inputs.js'
import { useRefreshModelState, useService, useSettingsState } from '../util/services.js'
import { useIsDark, useRefreshModelState, useService, useSettingsState } from '../util/services.js'
@ -44,7 +44,7 @@ export const ModelMenu = () => {
{modelDump.map(m => {
const { isHidden, isDefault, modelName, providerName } = m
return <div key={`${modelName}${providerName}`} className='flex items-center justify-between gap-4'>
return <div key={`${modelName}${providerName}`} className='flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-white/10'>
<span>{modelName} {isDefault ? '' : '(custom)'}</span>
<span>{providerName}</span>
<span onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>{isHidden ? 'hidden' : '✅'}</span>
@ -59,7 +59,7 @@ export const ModelMenu = () => {
const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => {
const { title, type, placeholder } = displayInfoOfSettingName(providerName, settingName)
const { title, placeholder } = displayInfoOfSettingName(providerName, settingName)
const voidSettingsService = useService('settingsStateService')
@ -110,7 +110,6 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
export const VoidProviderSettings = () => {
return <>
{providerNames.map(providerName =>
<SettingsForProvider key={providerName} providerName={providerName} />
@ -123,7 +122,8 @@ export const VoidProviderSettings = () => {
// full settings
export const Settings = () => {
return <div className='@@void-scope'>
const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''} px-2 lg:px-10`}>
<div className='w-full h-full'>
<div className='max-w-3xl mx-auto'>

View file

@ -5,6 +5,7 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'selector', // '{prefix-}dark' className is used to identify `dark:`
content: ['./src2/**/*.{jsx,tsx}'], // uses these files to decide how to transform the css file
theme: {
extend: {

View file

@ -76,6 +76,7 @@ class VoidSettingsPane extends EditorPane {
// parent.style.overflow = 'auto'
parent.style.userSelect = 'text'
// gets set immediately
this.instantiationService.invokeFunction(accessor => {
const services = getReactServices(accessor)
@ -91,6 +92,9 @@ class VoidSettingsPane extends EditorPane {
container.style.width = `${dimension.width}px`;
container.style.height = `${dimension.height}px`;
}
override get minimumWidth() { return 512 }
}