feat(app): restyle revert dock for v2 (#35560)
This commit is contained in:
@@ -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 { Button } from "@opencode-ai/ui/button"
|
||||||
import { DockTray } from "@opencode-ai/ui/dock-surface"
|
import { DockTray } from "@opencode-ai/ui/dock-surface"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
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 { useLanguage } from "@/context/language"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
|
|
||||||
export function SessionRevertDock(props: {
|
export function SessionRevertDock(props: {
|
||||||
items: { id: string; text: string }[]
|
items: { id: string; text: string }[]
|
||||||
@@ -12,6 +16,7 @@ export function SessionRevertDock(props: {
|
|||||||
onRestore: (id: string) => void
|
onRestore: (id: string) => void
|
||||||
}) {
|
}) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const settings = useSettings()
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
})
|
})
|
||||||
@@ -31,69 +36,153 @@ export function SessionRevertDock(props: {
|
|||||||
)
|
)
|
||||||
const preview = createMemo(() => props.items[0]?.text ?? "")
|
const preview = createMemo(() => props.items[0]?.text ?? "")
|
||||||
|
|
||||||
|
const onHeaderKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return
|
||||||
|
event.preventDefault()
|
||||||
|
toggle()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
class="pl-3 pr-2 py-2 flex items-center gap-2"
|
data-component="session-revert-dock"
|
||||||
role="button"
|
class="w-full overflow-hidden rounded-xl border-[0.5px] border-v2-border-border-base bg-v2-background-bg-layer-01"
|
||||||
tabIndex={0}
|
|
||||||
onClick={toggle}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key !== "Enter" && event.key !== " ") return
|
|
||||||
event.preventDefault()
|
|
||||||
toggle()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span class="shrink-0 text-14-regular text-text-strong cursor-default">{label()}</span>
|
<div
|
||||||
<Show when={store.collapsed && preview()}>
|
class="flex h-[42px] items-center gap-2 pl-4 pr-2"
|
||||||
<span class="min-w-0 flex-1 truncate text-14-regular text-text-base cursor-default">{preview()}</span>
|
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>
|
</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>
|
</div>
|
||||||
|
</Show>
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ const dict: Record<string, string> = {
|
|||||||
"session.todo.title": "Todos",
|
"session.todo.title": "Todos",
|
||||||
"session.todo.collapse": "Collapse todos",
|
"session.todo.collapse": "Collapse todos",
|
||||||
"session.todo.expand": "Expand 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.loading": "Loading prompt...",
|
||||||
"prompt.placeholder.normal": "Ask anything...",
|
"prompt.placeholder.normal": "Ask anything...",
|
||||||
"prompt.placeholder.simple": "Ask anything...",
|
"prompt.placeholder.simple": "Ask anything...",
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ const icons = {
|
|||||||
viewBox: "0 0 20 20",
|
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"/>`,
|
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: {
|
archive: {
|
||||||
viewBox: "0 0 16 16",
|
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"/>`,
|
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"/>`,
|
||||||
|
|||||||
Reference in New Issue
Block a user