feat: small transitions, spacing, polishing
This commit is contained in:
parent
0beb8a0bf6
commit
d6dbdb0b40
9 changed files with 131 additions and 35 deletions
|
|
@ -1557,7 +1557,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative p-3 flex items-center justify-between">
|
<div class="relative p-3 flex items-center justify-between">
|
||||||
<div class="flex items-center justify-start gap-0.5">
|
<div class="flex items-center justify-start gap-1">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={store.mode === "shell"}>
|
<Match when={store.mode === "shell"}>
|
||||||
<div class="flex items-center gap-2 px-2 h-6">
|
<div class="flex items-center gap-2 px-2 h-6">
|
||||||
|
|
@ -1608,13 +1608,60 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
title="Thinking effort"
|
title="Thinking effort"
|
||||||
keybind={command.keybind("model.variant.cycle")}
|
keybind={command.keybind("model.variant.cycle")}
|
||||||
>
|
>
|
||||||
<Button
|
{(() => {
|
||||||
variant="ghost"
|
const [text, setText] = createSignal(local.model.variant.current() ?? "Default")
|
||||||
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
|
const [animating, setAnimating] = createSignal(false)
|
||||||
onClick={() => local.model.variant.cycle()}
|
let locked = false
|
||||||
>
|
|
||||||
{local.model.variant.current() ?? "Default"}
|
const handleClick = async () => {
|
||||||
</Button>
|
if (locked) return
|
||||||
|
|
||||||
|
local.model.variant.cycle()
|
||||||
|
const newText = local.model.variant.current() ?? "Default"
|
||||||
|
|
||||||
|
if (newText === text()) return
|
||||||
|
|
||||||
|
locked = true
|
||||||
|
setAnimating(true)
|
||||||
|
|
||||||
|
// Wait for exit animation
|
||||||
|
const charCount = text().length
|
||||||
|
await new Promise((r) => setTimeout(r, charCount * 40 + 400))
|
||||||
|
|
||||||
|
// Reset animating before setting new text so @starting-style works
|
||||||
|
setAnimating(false)
|
||||||
|
setText(newText)
|
||||||
|
|
||||||
|
// Wait for enter animation
|
||||||
|
const newCharCount = newText.length
|
||||||
|
await new Promise((r) => setTimeout(r, newCharCount * 40 + 400))
|
||||||
|
|
||||||
|
locked = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="text-text-base _hidden text-12-regular"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<span data-slot="cycle-text" data-animating={animating()}>
|
||||||
|
<For each={text().split("")}>
|
||||||
|
{(char, i) =>
|
||||||
|
char === " " ? (
|
||||||
|
<span data-slot="space" />
|
||||||
|
) : (
|
||||||
|
<span data-slot="char" style={{ "--i": i() }}>
|
||||||
|
{i() === 0 ? char.toUpperCase() : char}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</For>
|
||||||
|
</span>
|
||||||
|
<Icon name="chevron-down" size="small" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={permission.permissionsEnabled() && params.id}>
|
<Show when={permission.permissionsEnabled() && params.id}>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { Session, type Message, type TextPart } from "@opencode-ai/sdk/v2/client"
|
import { Session, type Message, type TextPart, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import {
|
import {
|
||||||
|
|
@ -1352,7 +1352,7 @@ export default function Layout(props: ParentProps) {
|
||||||
})
|
})
|
||||||
|
|
||||||
const hoverMessages = createMemo(() =>
|
const hoverMessages = createMemo(() =>
|
||||||
sessionStore.message[props.session.id]?.filter((message) => message.role === "user"),
|
sessionStore.message[props.session.id]?.filter((message) => message.role === "user") as UserMessage[],
|
||||||
)
|
)
|
||||||
const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined)
|
const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined)
|
||||||
const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened())
|
const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened())
|
||||||
|
|
@ -1425,8 +1425,8 @@ export default function Layout(props: ParentProps) {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<HoverCard openDelay={150} closeDelay={100} placement="right" gutter={12} trigger={item}>
|
<HoverCard openDelay={150} closeDelay={100} placement="right" gutter={28} trigger={item}>
|
||||||
<Show when={hoverReady()} fallback={<div class="text-12-regular text-text-weak">Loading messages…</div>}>
|
<Show when={hoverReady()} fallback={<div>Loading messages…</div>}>
|
||||||
<MessageNav
|
<MessageNav
|
||||||
messages={hoverMessages() ?? []}
|
messages={hoverMessages() ?? []}
|
||||||
current={undefined}
|
current={undefined}
|
||||||
|
|
@ -1822,7 +1822,7 @@ export default function Layout(props: ParentProps) {
|
||||||
class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar"
|
class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar"
|
||||||
style={{ "overflow-anchor": "none" }}
|
style={{ "overflow-anchor": "none" }}
|
||||||
>
|
>
|
||||||
<nav class="flex flex-col gap-1 px-2">
|
<nav class="flex flex-col gap-2 px-2">
|
||||||
<Show when={loading()}>
|
<Show when={loading()}>
|
||||||
<SessionSkeleton />
|
<SessionSkeleton />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -1180,7 +1180,7 @@ export default function Page() {
|
||||||
if (isDesktop()) scheduleScrollSpy(e.currentTarget)
|
if (isDesktop()) scheduleScrollSpy(e.currentTarget)
|
||||||
}}
|
}}
|
||||||
onClick={autoScroll.handleInteraction}
|
onClick={autoScroll.handleInteraction}
|
||||||
class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar snap-y snap-mandatory"
|
class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar snap-both snap-mandatory"
|
||||||
style={{ "--session-title-height": info()?.title ? "40px" : "0px" }}
|
style={{ "--session-title-height": info()?.title ? "40px" : "0px" }}
|
||||||
>
|
>
|
||||||
<Show when={info()?.title}>
|
<Show when={info()?.title}>
|
||||||
|
|
@ -1252,7 +1252,7 @@ export default function Page() {
|
||||||
id={anchor(message.id)}
|
id={anchor(message.id)}
|
||||||
data-message-id={message.id}
|
data-message-id={message.id}
|
||||||
classList={{
|
classList={{
|
||||||
"min-w-0 w-full max-w-full snap-start": true,
|
"min-w-0 w-full max-w-full snap-both": true,
|
||||||
"md:max-w-200": !showTabs(),
|
"md:max-w-200": !showTabs(),
|
||||||
"last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]":
|
"last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]":
|
||||||
platform.platform !== "desktop",
|
platform.platform !== "desktop",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
padding: 4px 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
transition-property: background-color, border-color, color, box-shadow;
|
transition-property: background-color, border-color, color, box-shadow;
|
||||||
transition-duration: 200ms;
|
transition-duration: 200ms;
|
||||||
|
|
@ -105,19 +106,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-size="small"] {
|
&[data-size="small"] {
|
||||||
height: 22px;
|
padding: 2px 10px;
|
||||||
padding: 0 8px;
|
|
||||||
&[data-icon] {
|
&[data-icon] {
|
||||||
padding: 0 12px 0 4px;
|
padding: 0 12px 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
line-height: var(--line-height-large);
|
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
/* text-12-medium */
|
/* text-12-medium */
|
||||||
font-family: var(--font-family-sans);
|
font-family: var(--font-family-sans);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-base);
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
line-height: var(--line-height-large); /* 166.667% */
|
line-height: var(--line-height-large); /* 166.667% */
|
||||||
|
|
@ -166,3 +163,39 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-slot="cycle-text"] {
|
||||||
|
display: inline-flex;
|
||||||
|
perspective: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
[data-slot="char"] {
|
||||||
|
display: inline-block;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transform-origin: 50% 100%;
|
||||||
|
transform: rotateX(0deg);
|
||||||
|
opacity: 1;
|
||||||
|
transition:
|
||||||
|
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-delay: calc(var(--i, 0) * 40ms);
|
||||||
|
|
||||||
|
/* Entry animation using @starting-style */
|
||||||
|
@starting-style {
|
||||||
|
transform: rotateX(90deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exit animation when animating */
|
||||||
|
&[data-animating="true"] [data-slot="char"] {
|
||||||
|
transform: rotateX(-90deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preserve spaces */
|
||||||
|
[data-slot="space"] {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding-left: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
&[data-size="normal"] {
|
&[data-size="normal"] {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
gap: 4px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-size="compact"] {
|
&[data-size="compact"] {
|
||||||
|
|
@ -36,6 +37,7 @@
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
&[data-active] [data-slot="message-nav-tick-line"] {
|
&[data-active] [data-slot="message-nav-tick-line"] {
|
||||||
background-color: var(--icon-strong-base);
|
background-color: var(--icon-strong-base);
|
||||||
|
|
@ -54,7 +56,8 @@
|
||||||
|
|
||||||
[data-slot="message-nav-tick-button"]:hover [data-slot="message-nav-tick-line"] {
|
[data-slot="message-nav-tick-button"]:hover [data-slot="message-nav-tick-line"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--icon-strong-base);
|
color: var(--text-strong);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="message-nav-message-button"] {
|
[data-slot="message-nav-message-button"] {
|
||||||
|
|
@ -62,33 +65,38 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
color: inherit;
|
||||||
column-gap: 12px;
|
column-gap: 12px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 4px 12px;
|
padding: 0;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="message-nav-title-preview"] {
|
[data-slot="message-nav-title-preview"] {
|
||||||
font-size: 14px; /* text-14-regular */
|
font-size: 14px; /* text-14-regular */
|
||||||
color: var(--text-weak);
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
|
&:not(:hover) {
|
||||||
|
color: var(--text-weak);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
&[data-active] {
|
&[data-active] {
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="message-nav-item"]:hover [data-slot="message-nav-message-button"] {
|
[data-slot="message-nav-item"]:hover [data-slot="message-nav-message-button"] {
|
||||||
background-color: var(--surface-base);
|
color: var(--text-strong);
|
||||||
}
|
}
|
||||||
[data-slot="message-nav-item"]:active [data-slot="message-nav-message-button"] {
|
[data-slot="message-nav-item"]:active [data-slot="message-nav-message-button"] {
|
||||||
background-color: var(--surface-base-active);
|
color: var(--text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="message-nav-item"]:active [data-slot="message-nav-title-preview"] {
|
[data-slot="message-nav-item"]:active [data-slot="message-nav-title-preview"] {
|
||||||
|
|
@ -101,7 +109,7 @@
|
||||||
|
|
||||||
[data-slot="message-nav-tooltip-content"] {
|
[data-slot="message-nav-tooltip-content"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 4px 4px 6px 4px;
|
padding: 4px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { ComponentProps, For, Match, Show, splitProps, Switch } from "solid-js"
|
import { ComponentProps, For, Match, Show, splitProps, Switch } from "solid-js"
|
||||||
import { DiffChanges } from "./diff-changes"
|
import { DiffChanges } from "./diff-changes"
|
||||||
import { Tooltip } from "@kobalte/core/tooltip"
|
import { Tooltip } from "@kobalte/core/tooltip"
|
||||||
|
import { ScrollReveal } from "./scroll-reveal"
|
||||||
|
|
||||||
export function MessageNav(
|
export function MessageNav(
|
||||||
props: ComponentProps<"ul"> & {
|
props: ComponentProps<"ul"> & {
|
||||||
|
|
@ -43,14 +44,16 @@ export function MessageNav(
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={local.size === "normal"}>
|
<Match when={local.size === "normal"}>
|
||||||
<button data-slot="message-nav-message-button" onClick={handleClick} onKeyDown={handleKeyPress}>
|
<button data-slot="message-nav-message-button" onClick={handleClick} onKeyDown={handleKeyPress}>
|
||||||
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
|
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" class="-ml-1" />
|
||||||
<div
|
<div
|
||||||
data-slot="message-nav-title-preview"
|
data-slot="message-nav-title-preview"
|
||||||
data-active={message.id === local.current?.id || undefined}
|
data-active={message.id === local.current?.id || undefined}
|
||||||
>
|
>
|
||||||
<Show when={local.getLabel?.(message) ?? message.summary?.title} fallback="New message">
|
<ScrollReveal fadeEndSize={12}>
|
||||||
{local.getLabel?.(message) ?? message.summary?.title}
|
<Show when={local.getLabel?.(message) ?? message.summary?.title} fallback="New message">
|
||||||
</Show>
|
{local.getLabel?.(message) ?? message.summary?.title}
|
||||||
|
</Show>
|
||||||
|
</ScrollReveal>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</Match>
|
</Match>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ export function ScrollReveal(props: ScrollRevealProps) {
|
||||||
local.ref?.(el)
|
local.ref?.(el)
|
||||||
}}
|
}}
|
||||||
fadeStartSize={8}
|
fadeStartSize={8}
|
||||||
fadeEndSize={16}
|
fadeEndSize={8}
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
[data-component="select"] {
|
[data-component="select"] {
|
||||||
[data-slot="select-select-trigger"] {
|
[data-slot="select-select-trigger"] {
|
||||||
padding: 0 4px 0 8px;
|
display: flex;
|
||||||
|
padding: 4px 8px !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transition: background-color 200ms cubic-bezier(0.25, 0, 0.5, 1);
|
transition: background-color 200ms cubic-bezier(0.25, 0, 0.5, 1);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue