mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-26 08:32:01 +00:00
fix(app): animate composer delivery controls (#44886)
This commit is contained in:
parent
5ad0f0dc5a
commit
1f7ae3f638
4 changed files with 85 additions and 17 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import { createEffect, createMemo, For, Show, type JSX } from "solid-js"
|
||||
import { createEffect, createMemo, createSignal, For, Show, type JSX } from "solid-js"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { createAnimatedPresence } from "@/runtime/animated-presence"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { useI18n } from "@opencode-ai/ui/context/i18n"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
|
|
@ -716,16 +717,23 @@ function ComposerEditorAlternateDelivery(props: { controller: ComposerEditorMode
|
|||
if (queue.editing()) return "steer" as const
|
||||
return queue.alternate()
|
||||
})
|
||||
const [button, setButton] = createSignal<HTMLButtonElement>()
|
||||
const presence = createAnimatedPresence(action, () => button() ?? null)
|
||||
return (
|
||||
<Show when={action()} keyed>
|
||||
<Show when={presence.present() && presence.value()} keyed>
|
||||
{(delivery) => (
|
||||
<Tooltip placement="top" inactive={delivery !== "steer"} value={i18n.t("ui.promptInput.steerHint")}>
|
||||
<Button
|
||||
ref={setButton}
|
||||
data-action="composer-alternate-delivery"
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
class="me-3 gap-1.5 px-1.5 text-v2-text-text-muted ![font-weight:530]"
|
||||
class="me-3 gap-1.5 px-1.5 text-v2-text-text-muted ![font-weight:530] duration-150 motion-reduce:animate-none"
|
||||
classList={{
|
||||
"animate-in fade-in": presence.animate() && presence.show(),
|
||||
"animate-out fade-out fill-mode-forwards": presence.animate() && !presence.show(),
|
||||
}}
|
||||
onClick={() => props.controller.submit({ alternate: true })}
|
||||
>
|
||||
{delivery === "steer" ? i18n.t("ui.promptInput.steer") : i18n.t("ui.promptInput.queue")}
|
||||
|
|
|
|||
21
packages/app/src/runtime/animated-presence.ts
Normal file
21
packages/app/src/runtime/animated-presence.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { createMemo, type Accessor } from "solid-js"
|
||||
import createPresence from "solid-presence"
|
||||
|
||||
export function createAnimatedPresence<T>(value: Accessor<T | undefined>, element: Accessor<HTMLElement | null>) {
|
||||
const animation = createMemo<{ show: boolean; animate: boolean; value: T | undefined }>((previous) => {
|
||||
const current = value()
|
||||
const show = current !== undefined
|
||||
return {
|
||||
show,
|
||||
animate: previous !== undefined && (previous.animate || previous.show !== show),
|
||||
value: current ?? previous?.value,
|
||||
}
|
||||
})
|
||||
const presence = createPresence({ show: () => animation().show, element })
|
||||
return {
|
||||
...presence,
|
||||
show: () => animation().show,
|
||||
animate: () => animation().animate,
|
||||
value: () => animation().value,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { createEffect, createMemo, createSignal, For, on, Show, type Accessor, type JSX } from "solid-js"
|
||||
import createPresence from "solid-presence"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createAnimatedPresence } from "@/runtime/animated-presence"
|
||||
import type { SessionUserActions } from "@opencode-ai/session-ui/actions"
|
||||
import { Badge } from "@opencode-ai/ui/badge"
|
||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||
|
|
@ -479,17 +479,7 @@ function MessageTimelineView(
|
|||
return row.group.ref.partID
|
||||
})
|
||||
const [backgroundHintRef, setBackgroundHintRef] = createSignal<HTMLDivElement>()
|
||||
const backgroundHintVisibility = createMemo<{ show: boolean; animate: boolean }>(
|
||||
(previous) => {
|
||||
const show = backgroundHintPartID() !== undefined
|
||||
return { show, animate: previous.animate || previous.show !== show }
|
||||
},
|
||||
{ show: backgroundHintPartID() !== undefined, animate: false },
|
||||
)
|
||||
const backgroundHintPresence = createPresence({
|
||||
show: () => backgroundHintVisibility().show,
|
||||
element: () => backgroundHintRef() ?? null,
|
||||
})
|
||||
const backgroundHintPresence = createAnimatedPresence(backgroundHintPartID, () => backgroundHintRef() ?? null)
|
||||
return (
|
||||
<VirtualizedTimeline
|
||||
workspaceSession={workspaceSession}
|
||||
|
|
@ -507,9 +497,9 @@ function MessageTimelineView(
|
|||
class="duration-150 motion-reduce:animate-none"
|
||||
classList={{
|
||||
[`flex h-9 items-start pt-3 ${turnPadding()}`]: true,
|
||||
"animate-in fade-in": backgroundHintVisibility().animate && backgroundHintVisibility().show,
|
||||
"animate-in fade-in": backgroundHintPresence.animate() && backgroundHintPresence.show(),
|
||||
"animate-out fade-out fill-mode-forwards":
|
||||
backgroundHintVisibility().animate && !backgroundHintVisibility().show,
|
||||
backgroundHintPresence.animate() && !backgroundHintPresence.show(),
|
||||
}}
|
||||
>
|
||||
<BackgroundMoveHint />
|
||||
|
|
|
|||
49
packages/app/test-browser/animated-presence-state.test.ts
Normal file
49
packages/app/test-browser/animated-presence-state.test.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { createAnimatedPresence } from "../src/runtime/animated-presence"
|
||||
import { createRoot, createSignal } from "solid-js"
|
||||
|
||||
test("animates visibility changes without animating initial presence", () => {
|
||||
createRoot((dispose) => {
|
||||
const [value, setValue] = createSignal<string | undefined>("steer")
|
||||
const presence = createAnimatedPresence(value, () => null)
|
||||
|
||||
expect(presence.show()).toBe(true)
|
||||
expect(presence.animate()).toBe(false)
|
||||
expect(presence.value()).toBe("steer")
|
||||
expect(presence.present()).toBe(true)
|
||||
|
||||
setValue("queue")
|
||||
expect(presence.animate()).toBe(false)
|
||||
expect(presence.value()).toBe("queue")
|
||||
|
||||
setValue(undefined)
|
||||
expect(presence.show()).toBe(false)
|
||||
expect(presence.animate()).toBe(true)
|
||||
expect(presence.value()).toBe("queue")
|
||||
|
||||
setValue("steer")
|
||||
expect(presence.show()).toBe(true)
|
||||
expect(presence.animate()).toBe(true)
|
||||
expect(presence.value()).toBe("steer")
|
||||
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("animates the first appearance when initially hidden", () => {
|
||||
createRoot((dispose) => {
|
||||
const [value, setValue] = createSignal<string | undefined>()
|
||||
const presence = createAnimatedPresence(value, () => null)
|
||||
|
||||
expect(presence.show()).toBe(false)
|
||||
expect(presence.animate()).toBe(false)
|
||||
expect(presence.present()).toBe(false)
|
||||
|
||||
setValue("steer")
|
||||
expect(presence.show()).toBe(true)
|
||||
expect(presence.animate()).toBe(true)
|
||||
expect(presence.value()).toBe("steer")
|
||||
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue