mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-09 23:38:29 +00:00
feat(app): restyle revert dock for v2 (#35560)
This commit is contained in:
parent
ae7d63272c
commit
4f9207daac
4 changed files with 275 additions and 59 deletions
|
|
@ -0,0 +1,118 @@
|
|||
import { For } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
|
||||
import { SettingsProvider, useSettings } from "@/context/settings"
|
||||
|
||||
export default {
|
||||
title: "Composer/Revert Dock",
|
||||
id: "composer-revert-dock",
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `### Overview
|
||||
Real \`SessionRevertDock\` from app code, rendered above a mock composer card.
|
||||
|
||||
### Source path
|
||||
- \`packages/app/src/pages/session/composer/session-revert-dock.tsx\`
|
||||
|
||||
### Why the mock composer
|
||||
The live composer overlaps the dock's bottom by 18px (\`session-composer-region-controller.ts\` \`lift()\`).
|
||||
The card below reproduces that overlap so the collapsed/expanded cutoff behavior can be verified in isolation.
|
||||
|
||||
### Layout split
|
||||
Use the **Layout** button to toggle \`newLayoutDesigns\` and preview both the v2 dock and the legacy (v1) \`DockTray\` fallback.
|
||||
|
||||
### Notes
|
||||
- \`onRestore\` only mutates local story state, so nothing in the real session is affected.
|
||||
- Click the header to expand/collapse. Click "Restore message" to remove a row.`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const messages = [
|
||||
"update current branch with latest changes from dev and fix conflicts if any",
|
||||
"investigate why the chat input loses focus after sending a message",
|
||||
"Debug why streaming responses sometimes duplicate the last token",
|
||||
"suggest a better title for this PR based on the diff",
|
||||
"add a storybook story for the revert dock",
|
||||
"reduce re-renders in the timeline component",
|
||||
]
|
||||
|
||||
const btn = (accent?: boolean) =>
|
||||
({
|
||||
padding: "6px 12px",
|
||||
"border-radius": "6px",
|
||||
border: "1px solid var(--v2-border-border-base, #0000001a)",
|
||||
background: accent ? "var(--v2-background-bg-contrast, #242424)" : "var(--v2-background-bg-base, #fff)",
|
||||
color: accent ? "var(--v2-text-text-contrast, #fafafa)" : "var(--v2-text-text-base, #161616)",
|
||||
cursor: "pointer",
|
||||
"font-size": "13px",
|
||||
}) as const
|
||||
|
||||
function Stage(props: { count: number }) {
|
||||
const settings = useSettings()
|
||||
const seed = () => messages.slice(0, props.count).map((text, index) => ({ id: `rolled-${index}`, text }))
|
||||
const [store, setStore] = createStore({ items: seed() })
|
||||
|
||||
const v2 = () => settings.general.newLayoutDesigns()
|
||||
const reset = () => setStore("items", seed())
|
||||
const restore = (id: string) =>
|
||||
setStore(
|
||||
"items",
|
||||
store.items.filter((item) => item.id !== id),
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ display: "grid", gap: "16px", "max-width": "720px" }}>
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<button style={btn()} onClick={reset}>
|
||||
Reset ({props.count})
|
||||
</button>
|
||||
<button style={btn(v2())} onClick={() => settings.general.setNewLayoutDesigns(!v2())}>
|
||||
Layout: {v2() ? "v2" : "v1"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Reproduce the real composer stack: dock + card overlapping the dock's bottom by lift() = 18px */}
|
||||
<div style={{ display: "flex", "flex-direction": "column" }}>
|
||||
<SessionRevertDock items={store.items} onRestore={restore} />
|
||||
<div
|
||||
style={{ position: "relative", "z-index": 70, "margin-top": "-18px" }}
|
||||
class="min-h-24 w-full rounded-[12px] border border-v2-border-border-base bg-v2-background-bg-base px-4 py-3 text-[13px] text-v2-text-text-faint"
|
||||
>
|
||||
Ask anything...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-[12px] text-v2-text-text-faint">
|
||||
Restored so far:{" "}
|
||||
<For each={seed()}>
|
||||
{(item) => <span>{store.items.some((current) => current.id === item.id) ? "" : `“${item.text}” `}</span>}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const story = (count: number) => () => (
|
||||
<SettingsProvider>
|
||||
<Stage count={count} />
|
||||
</SettingsProvider>
|
||||
)
|
||||
|
||||
export const OneMessage = {
|
||||
name: "1 rolled back",
|
||||
render: story(1),
|
||||
}
|
||||
|
||||
export const ThreeMessages = {
|
||||
name: "3 rolled back",
|
||||
render: story(3),
|
||||
}
|
||||
|
||||
export const ManyMessages = {
|
||||
name: "6 rolled back (scrolls)",
|
||||
render: story(6),
|
||||
}
|
||||
|
|
@ -3,7 +3,11 @@ import { createStore } from "solid-js/store"
|
|||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DockTray } from "@opencode-ai/ui/dock-surface"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings } from "@/context/settings"
|
||||
|
||||
export function SessionRevertDock(props: {
|
||||
items: { id: string; text: string }[]
|
||||
|
|
@ -12,6 +16,7 @@ export function SessionRevertDock(props: {
|
|||
onRestore: (id: string) => void
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
const [store, setStore] = createStore({
|
||||
collapsed: true,
|
||||
})
|
||||
|
|
@ -31,69 +36,153 @@ export function SessionRevertDock(props: {
|
|||
)
|
||||
const preview = createMemo(() => props.items[0]?.text ?? "")
|
||||
|
||||
const onHeaderKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
toggle()
|
||||
}
|
||||
|
||||
return (
|
||||
<DockTray data-component="session-revert-dock">
|
||||
<Show
|
||||
when={settings.general.newLayoutDesigns()}
|
||||
fallback={
|
||||
<DockTray data-component="session-revert-dock">
|
||||
<div
|
||||
class="pl-3 pr-2 py-2 flex items-center gap-2"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={toggle}
|
||||
onKeyDown={onHeaderKeyDown}
|
||||
>
|
||||
<span class="shrink-0 text-14-regular text-text-strong cursor-default">{label()}</span>
|
||||
<Show when={store.collapsed && preview()}>
|
||||
<span class="min-w-0 flex-1 truncate text-14-regular text-text-base cursor-default">{preview()}</span>
|
||||
</Show>
|
||||
<div class="ml-auto shrink-0">
|
||||
<IconButton
|
||||
icon="chevron-down"
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
toggle()
|
||||
}}
|
||||
aria-label={
|
||||
store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={store.collapsed}>
|
||||
<div class="h-5" aria-hidden="true" />
|
||||
</Show>
|
||||
|
||||
<Show when={!store.collapsed}>
|
||||
<div class="px-3 pb-7 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar">
|
||||
<For each={props.items}>
|
||||
{(item) => (
|
||||
<div class="flex items-center gap-2 min-w-0 py-1">
|
||||
<span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
class="shrink-0"
|
||||
disabled={props.disabled || !!props.restoring}
|
||||
onClick={() => props.onRestore(item.id)}
|
||||
>
|
||||
{language.t("session.revertDock.restore")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</DockTray>
|
||||
}
|
||||
>
|
||||
<div
|
||||
class="pl-3 pr-2 py-2 flex items-center gap-2"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={toggle}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
toggle()
|
||||
}}
|
||||
data-component="session-revert-dock"
|
||||
class="w-full overflow-hidden rounded-xl border-[0.5px] border-v2-border-border-base bg-v2-background-bg-layer-01"
|
||||
>
|
||||
<span class="shrink-0 text-14-regular text-text-strong cursor-default">{label()}</span>
|
||||
<Show when={store.collapsed && preview()}>
|
||||
<span class="min-w-0 flex-1 truncate text-14-regular text-text-base cursor-default">{preview()}</span>
|
||||
<div
|
||||
class="flex h-[42px] items-center gap-2 pl-4 pr-2"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={toggle}
|
||||
onKeyDown={onHeaderKeyDown}
|
||||
>
|
||||
<IconV2 name="outline-reset" size="normal" class="text-v2-icon-icon-muted" />
|
||||
<span
|
||||
classList={{
|
||||
"font-[440] shrink-0 cursor-default text-[13px] leading-5 tracking-[-0.04px]": true,
|
||||
"text-v2-text-text-base": !store.collapsed,
|
||||
"text-v2-text-text-muted": store.collapsed,
|
||||
}}
|
||||
>
|
||||
{label()}
|
||||
</span>
|
||||
<Show when={store.collapsed && preview()}>
|
||||
<span class="min-w-0 flex-1 truncate cursor-default text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint">
|
||||
{preview()}
|
||||
</span>
|
||||
</Show>
|
||||
<div class="ml-auto shrink-0">
|
||||
<IconButtonV2
|
||||
icon={<IconV2 name="outline-chevron-down" size="small" />}
|
||||
size="large"
|
||||
variant="ghost-muted"
|
||||
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
toggle()
|
||||
}}
|
||||
aria-label={
|
||||
store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sacrificial space the composer overlaps via its negative lift (18px), so the header stays fully visible */}
|
||||
<Show when={store.collapsed}>
|
||||
<div class="h-[18px]" aria-hidden="true" />
|
||||
</Show>
|
||||
|
||||
<Show when={!store.collapsed}>
|
||||
{/* Scroll viewport ends above the composer; the 18px sacrificial below is what the composer overlaps */}
|
||||
<div class="flex max-h-42 flex-col gap-2 overflow-y-auto px-4 pt-px pb-3 no-scrollbar">
|
||||
<For each={props.items}>
|
||||
{(item) => (
|
||||
<div class="flex h-6 min-w-0 items-center gap-2">
|
||||
<span class="min-w-0 flex-1 truncate text-[13px] font-[400] leading-5 tracking-[-0.04px] text-v2-text-text-muted">
|
||||
{item.text}
|
||||
</span>
|
||||
<ButtonV2
|
||||
size="small"
|
||||
variant="neutral"
|
||||
class="shrink-0"
|
||||
disabled={props.disabled || !!props.restoring}
|
||||
onClick={() => props.onRestore(item.id)}
|
||||
>
|
||||
{language.t("session.revertDock.restore")}
|
||||
</ButtonV2>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="h-[18px]" aria-hidden="true" />
|
||||
</Show>
|
||||
<div class="ml-auto shrink-0">
|
||||
<IconButton
|
||||
data-collapsed={store.collapsed ? "true" : "false"}
|
||||
icon="chevron-down"
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
toggle()
|
||||
}}
|
||||
aria-label={
|
||||
store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={store.collapsed}>
|
||||
<div class="h-5" aria-hidden="true" />
|
||||
</Show>
|
||||
|
||||
<Show when={!store.collapsed}>
|
||||
<div class="px-3 pb-7 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar">
|
||||
<For each={props.items}>
|
||||
{(item) => (
|
||||
<div class="flex items-center gap-2 min-w-0 py-1">
|
||||
<span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
class="shrink-0"
|
||||
disabled={props.disabled || !!props.restoring}
|
||||
onClick={() => props.onRestore(item.id)}
|
||||
>
|
||||
{language.t("session.revertDock.restore")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</DockTray>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ const dict: Record<string, string> = {
|
|||
"session.todo.title": "Todos",
|
||||
"session.todo.collapse": "Collapse todos",
|
||||
"session.todo.expand": "Expand todos",
|
||||
"session.revertDock.summary.one": "{{count}} rolled back message",
|
||||
"session.revertDock.summary.other": "{{count}} rolled back messages",
|
||||
"session.revertDock.collapse": "Collapse rolled back messages",
|
||||
"session.revertDock.expand": "Expand rolled back messages",
|
||||
"session.revertDock.restore": "Restore message",
|
||||
"prompt.loading": "Loading prompt...",
|
||||
"prompt.placeholder.normal": "Ask anything...",
|
||||
"prompt.placeholder.simple": "Ask anything...",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,10 @@ const icons = {
|
|||
viewBox: "0 0 20 20",
|
||||
body: `<path d="M5.83333 4.16406L2.5 7.4974L5.83333 10.8307M3.33333 7.4974H17.9167V15.4141H10" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
},
|
||||
"outline-reset": {
|
||||
viewBox: "0 0 20 20",
|
||||
body: `<path d="M5.83333 4.16406L2.5 7.4974L5.83333 10.8307M3.33333 7.4974H17.9167V15.4141H10" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
},
|
||||
archive: {
|
||||
viewBox: "0 0 16 16",
|
||||
body: `<path d="M13.1112 13.5555V14.0555H13.6112V13.5555H13.1112ZM2.889 13.5555H2.389L2.389 14.0555H2.889V13.5555ZM3.38901 5.55546L3.38901 5.05546L2.38901 5.05546L2.38901 5.55546L2.88901 5.55546L3.38901 5.55546ZM14.4446 2.44434H14.9446V1.94434L14.4446 1.94434L14.4446 2.44434ZM14.4446 5.55545L14.4446 6.05545L14.9446 6.05545V5.55545H14.4446ZM1.55566 5.55546L1.05566 5.55545L1.05566 6.05546L1.55566 6.05546L1.55566 5.55546ZM1.5557 2.44436L1.5557 1.94436L1.05571 1.94436L1.0557 2.44435L1.5557 2.44436ZM13.1112 5.55546H12.6112V13.5555H13.1112H13.6112V5.55546H13.1112ZM2.889 13.5555H3.389L3.38901 5.55546L2.88901 5.55546L2.38901 5.55546L2.389 13.5555H2.889ZM14.4446 2.44434H13.9446V5.55545H14.4446H14.9446V2.44434H14.4446ZM1.55566 5.55546L2.05566 5.55547L2.0557 2.44436L1.5557 2.44436L1.0557 2.44435L1.05566 5.55545L1.55566 5.55546ZM6.22234 8.22213V8.72213H9.7779V8.22213V7.72213H6.22234V8.22213ZM13.1112 13.5555V13.0555H2.889V13.5555V14.0555H13.1112V13.5555ZM1.5557 2.44436L1.5557 2.94436L14.4446 2.94434L14.4446 2.44434L14.4446 1.94434L1.5557 1.94436L1.5557 2.44436ZM14.4446 5.55545L14.4446 5.05545L1.55566 5.05546L1.55566 5.55546L1.55566 6.05546L14.4446 6.05545L14.4446 5.55545Z" fill="currentColor"/>`,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue