mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
feat(tui): add floating side panel for theme selector
Position theme dialog on right edge without backdrop overlay, allowing full visibility of theme changes in real-time.
This commit is contained in:
parent
c81506b28d
commit
60b730f7ca
3 changed files with 48 additions and 17 deletions
|
|
@ -16,6 +16,10 @@ export function DialogThemeList() {
|
|||
let ref: DialogSelectRef<string>
|
||||
const initial = theme.selected
|
||||
|
||||
onMount(() => {
|
||||
dialog.setPosition("floating-right")
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (!confirmed) theme.set(initial)
|
||||
})
|
||||
|
|
@ -25,6 +29,7 @@ export function DialogThemeList() {
|
|||
title="Themes"
|
||||
options={options}
|
||||
current={initial}
|
||||
compact
|
||||
onMove={(opt) => {
|
||||
theme.set(opt.value)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export interface DialogSelectProps<T> {
|
|||
onTrigger: (option: DialogSelectOption<T>) => void
|
||||
}[]
|
||||
current?: T
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
export interface DialogSelectOption<T = any> {
|
||||
|
|
@ -97,9 +98,13 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
})
|
||||
|
||||
const dimensions = useTerminalDimensions()
|
||||
const height = createMemo(() =>
|
||||
Math.min(flat().length + grouped().length * 2 - 1, Math.floor(dimensions().height / 2) - 6),
|
||||
)
|
||||
const height = createMemo(() => {
|
||||
const itemCount = flat().length + grouped().length * 2 - 1
|
||||
const maxHeight = props.compact
|
||||
? Math.floor(dimensions().height * 0.8) - 6
|
||||
: Math.floor(dimensions().height / 2) - 6
|
||||
return Math.min(itemCount, maxHeight)
|
||||
})
|
||||
|
||||
const selected = createMemo(() => flat()[store.selected])
|
||||
|
||||
|
|
@ -185,7 +190,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
|
||||
return (
|
||||
<box gap={1} paddingBottom={1}>
|
||||
<box paddingLeft={4} paddingRight={4}>
|
||||
<box paddingLeft={props.compact ? 2 : 4} paddingRight={props.compact ? 2 : 4}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||
{props.title}
|
||||
|
|
@ -257,6 +262,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
active={active()}
|
||||
current={current()}
|
||||
gutter={option.gutter}
|
||||
compact={props.compact}
|
||||
/>
|
||||
</box>
|
||||
)
|
||||
|
|
@ -292,6 +298,7 @@ function Option(props: {
|
|||
footer?: JSX.Element | string
|
||||
gutter?: JSX.Element
|
||||
onMouseOver?: () => void
|
||||
compact?: boolean
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const fg = selectedForeground(theme)
|
||||
|
|
@ -315,7 +322,7 @@ function Option(props: {
|
|||
overflow="hidden"
|
||||
paddingLeft={3}
|
||||
>
|
||||
{Locale.truncate(props.title, 61)}
|
||||
{Locale.truncate(props.title, props.compact ? 24 : 61)}
|
||||
<Show when={props.description}>
|
||||
<span style={{ fg: props.active ? fg : theme.textMuted }}> {props.description}</span>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -6,15 +6,19 @@ import { createStore } from "solid-js/store"
|
|||
import { Clipboard } from "@tui/util/clipboard"
|
||||
import { useToast } from "./toast"
|
||||
|
||||
export type DialogPosition = "center" | "floating-right"
|
||||
|
||||
export function Dialog(
|
||||
props: ParentProps<{
|
||||
size?: "medium" | "large"
|
||||
position?: DialogPosition
|
||||
onClose: () => void
|
||||
}>,
|
||||
) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const { theme } = useTheme()
|
||||
const renderer = useRenderer()
|
||||
const isFloating = () => props.position === "floating-right"
|
||||
|
||||
return (
|
||||
<box
|
||||
|
|
@ -24,20 +28,24 @@ export function Dialog(
|
|||
}}
|
||||
width={dimensions().width}
|
||||
height={dimensions().height}
|
||||
alignItems="center"
|
||||
alignItems={isFloating() ? "flex-end" : "center"}
|
||||
justifyContent={isFloating() ? "flex-end" : undefined}
|
||||
position="absolute"
|
||||
paddingTop={dimensions().height / 4}
|
||||
paddingTop={isFloating() ? 0 : dimensions().height / 4}
|
||||
paddingRight={isFloating() ? 1 : 0}
|
||||
paddingBottom={isFloating() ? 1 : 0}
|
||||
left={0}
|
||||
top={0}
|
||||
backgroundColor={RGBA.fromInts(0, 0, 0, 150)}
|
||||
backgroundColor={isFloating() ? RGBA.fromInts(0, 0, 0, 0) : RGBA.fromInts(0, 0, 0, 150)}
|
||||
>
|
||||
<box
|
||||
onMouseUp={async (e) => {
|
||||
if (renderer.getSelection()) return
|
||||
e.stopPropagation()
|
||||
}}
|
||||
width={props.size === "large" ? 80 : 60}
|
||||
width={isFloating() ? 30 : props.size === "large" ? 80 : 60}
|
||||
maxWidth={dimensions().width - 2}
|
||||
maxHeight={isFloating() ? Math.floor(dimensions().height * 0.8) : undefined}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
paddingTop={1}
|
||||
>
|
||||
|
|
@ -54,6 +62,7 @@ function init() {
|
|||
onClose?: () => void
|
||||
}[],
|
||||
size: "medium" as "medium" | "large",
|
||||
position: "center" as DialogPosition,
|
||||
})
|
||||
|
||||
useKeyboard((evt) => {
|
||||
|
|
@ -92,6 +101,7 @@ function init() {
|
|||
}
|
||||
batch(() => {
|
||||
setStore("size", "medium")
|
||||
setStore("position", "center")
|
||||
setStore("stack", [])
|
||||
})
|
||||
refocus()
|
||||
|
|
@ -103,13 +113,16 @@ function init() {
|
|||
for (const item of store.stack) {
|
||||
if (item.onClose) item.onClose()
|
||||
}
|
||||
setStore("size", "medium")
|
||||
setStore("stack", [
|
||||
{
|
||||
element: input,
|
||||
onClose,
|
||||
},
|
||||
])
|
||||
batch(() => {
|
||||
setStore("size", "medium")
|
||||
setStore("position", "center")
|
||||
setStore("stack", [
|
||||
{
|
||||
element: input,
|
||||
onClose,
|
||||
},
|
||||
])
|
||||
})
|
||||
},
|
||||
get stack() {
|
||||
return store.stack
|
||||
|
|
@ -117,9 +130,15 @@ function init() {
|
|||
get size() {
|
||||
return store.size
|
||||
},
|
||||
get position() {
|
||||
return store.position
|
||||
},
|
||||
setSize(size: "medium" | "large") {
|
||||
setStore("size", size)
|
||||
},
|
||||
setPosition(position: DialogPosition) {
|
||||
setStore("position", position)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +171,7 @@ export function DialogProvider(props: ParentProps) {
|
|||
}}
|
||||
>
|
||||
<Show when={value.stack.length}>
|
||||
<Dialog onClose={() => value.clear()} size={value.size}>
|
||||
<Dialog onClose={() => value.clear()} size={value.size} position={value.position}>
|
||||
{value.stack.at(-1)!.element}
|
||||
</Dialog>
|
||||
</Show>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue