feat: transitions, review, dialog

This commit is contained in:
Aaron Iker
2026-01-29 17:51:31 +01:00
parent f39ff9c4ae
commit 32d29ecbb1
8 changed files with 88 additions and 27 deletions

View File

@@ -88,7 +88,7 @@ const ModelList: Component<{
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
provider?: string
children?: JSX.Element
children?: JSX.Element | ((open: boolean) => JSX.Element)
triggerAs?: T
triggerProps?: ComponentProps<T>
}) {
@@ -101,13 +101,44 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
}
const language = useLanguage()
// Handle ESC key to close
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault()
e.stopPropagation()
setOpen(false)
}
}
const renderChildren = () => {
if (typeof props.children === "function") {
return props.children(open())
}
return props.children
}
return (
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
<Kobalte.Trigger as={props.triggerAs ?? "div"} {...(props.triggerProps as any)}>
{props.children}
<Kobalte.Trigger
as={props.triggerAs ?? "div"}
{...(props.triggerProps as any)}
data-active={open() ? "true" : undefined}
>
{renderChildren()}
</Kobalte.Trigger>
<Kobalte.Portal>
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
<Show when={open()}>
<div
class="fixed inset-0 z-40"
onClick={() => setOpen(false)}
onKeyDown={handleKeyDown}
/>
</Show>
<Kobalte.Content
class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
data-component="model-popover-content"
onKeyDown={handleKeyDown}
>
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
<ModelList
provider={props.provider}

View File

@@ -59,8 +59,8 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
}
const circle = () => (
<div class="p-1">
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
<div class="p-1 text-icon-base">
<ProgressCircle size={16} strokeWidth={1} percentage={context()?.percentage ?? 0} />
</div>
)
@@ -99,7 +99,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
<Button
type="button"
variant="ghost"
class="size-6"
class="size-6 text-icon-base"
onClick={openContext}
aria-label={language.t("context.usage.view")}
>

View File

@@ -24,11 +24,11 @@
}
&[data-closed] {
animation: hover-card-close 0.15s ease-out;
animation: hover-card-close var(--transition-duration) var(--transition-easing);
}
&[data-expanded] {
animation: hover-card-open 0.15s ease-out;
animation: hover-card-open var(--transition-duration) var(--transition-easing);
}
[data-slot="hover-card-body"] {

View File

@@ -7,6 +7,9 @@
user-select: none;
aspect-ratio: 1;
flex-shrink: 0;
transition-property: background-color, color, opacity, box-shadow;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
&[data-variant="primary"] {
background-color: var(--icon-strong-base);
@@ -99,7 +102,7 @@
/* color: var(--icon-active); */
/* } */
}
&:selected:not(:disabled) {
&[data-selected]:not(:disabled) {
background-color: var(--surface-raised-base-active);
/* [data-slot="icon-svg"] { */
/* color: var(--icon-selected); */

View File

@@ -27,12 +27,9 @@
content: "";
opacity: var(--indicator-opacity, 1);
position: absolute;
transition:
opacity 300ms ease-in-out,
box-shadow 100ms ease-in-out,
width 150ms ease,
height 150ms ease,
transform 150ms ease;
transition-property: opacity, box-shadow, width, height, transform;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
}
[data-slot="radio-group-item"] {
@@ -46,7 +43,9 @@
content: "";
inset: 6px 0;
position: absolute;
transition: opacity 150ms ease;
transition-property: opacity;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
width: 1px;
transform: translateX(-0.5px);
}
@@ -72,9 +71,9 @@
padding: 6px 12px;
place-content: center;
position: relative;
transition-duration: 150ms;
transition-property: color, opacity;
transition-timing-function: ease-in-out;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
user-select: none;
}

View File

@@ -6,7 +6,9 @@
content: "";
position: absolute;
opacity: 0;
transition: opacity 0.15s ease-in-out;
transition-property: opacity;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
}
&:hover::after,

View File

@@ -56,12 +56,8 @@
[data-slot="accordion-item"] {
[data-slot="accordion-content"] {
display: none;
}
&[data-expanded] {
[data-slot="accordion-content"] {
display: block;
}
/* Use grid-template-rows for smooth height transition */
display: grid;
}
}
@@ -185,7 +181,9 @@
cursor: pointer;
border-radius: 4px;
opacity: 0;
transition: opacity 0.15s ease;
transition-property: opacity, background-color, color;
transition-duration: var(--transition-duration);
transition-timing-function: var(--transition-easing);
&:hover {
color: var(--text-strong);

View File

@@ -25,10 +25,15 @@ const Context = createContext<ReturnType<typeof init>>()
function init() {
const [active, setActive] = createSignal<Active | undefined>()
const [renders, setRenders] = createSignal<Record<string, JSX.Element>>({})
const close = () => {
const current = active()
if (!current) return
setRenders((renders) => {
const { [current.id]: _, ...rest } = renders
return rest
})
current.onClose?.()
current.dispose()
setActive(undefined)
@@ -66,12 +71,28 @@ function init() {
setActive({ id, node, dispose, owner, onClose })
}
const render = (element: JSX.Element, id: string, owner: Owner) => {
setRenders((renders) => ({ ...renders, [id]: element }))
show(() => element, owner, () => {
setRenders((renders) => {
const { [id]: _, ...rest } = renders
return rest
})
})
}
const isActive = (id: string) => {
return renders()[id] !== undefined
}
return {
get active() {
return active()
},
isActive,
close,
show,
render,
}
}
@@ -100,10 +121,17 @@ export function useDialog() {
get active() {
return ctx.active
},
isActive(id: string) {
return ctx.isActive(id)
},
show(element: DialogElement, onClose?: () => void) {
const base = ctx.active?.owner ?? owner
ctx.show(element, base, onClose)
},
render(element: JSX.Element, id: string) {
const base = ctx.active?.owner ?? owner
ctx.render(element, id, base)
},
close() {
ctx.close()
},