mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-16 13:44:44 +00:00
fix(app): sidebar spacing + session list spinner transition (#17355)
This commit is contained in:
@@ -1961,7 +1961,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"flex flex-col min-h-0 min-w-0 box-border rounded-tl-[12px] px-2": true,
|
"flex flex-col min-h-0 min-w-0 box-border rounded-tl-[12px] px-3": true,
|
||||||
"border border-b-0 border-border-weak-base": !merged(),
|
"border border-b-0 border-border-weak-base": !merged(),
|
||||||
"border-l border-t border-border-weaker-base": merged(),
|
"border-l border-t border-border-weaker-base": merged(),
|
||||||
"bg-background-base": merged() || hover(),
|
"bg-background-base": merged() || hover(),
|
||||||
@@ -1976,8 +1976,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
<Show when={panelProps.project}>
|
<Show when={panelProps.project}>
|
||||||
{(p) => (
|
{(p) => (
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 px-2 py-1">
|
<div class="shrink-0 pl-1 py-1">
|
||||||
<div class="group/project flex items-start justify-between gap-2 p-2 pr-1">
|
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
|
||||||
<div class="flex flex-col min-w-0">
|
<div class="flex flex-col min-w-0">
|
||||||
<InlineEditor
|
<InlineEditor
|
||||||
id={`project:${projectId()}`}
|
id={`project:${projectId()}`}
|
||||||
@@ -2063,7 +2063,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
when={workspacesEnabled()}
|
when={workspacesEnabled()}
|
||||||
fallback={
|
fallback={
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 py-4 px-3">
|
<div class="shrink-0 py-4">
|
||||||
<Button
|
<Button
|
||||||
size="large"
|
size="large"
|
||||||
icon="plus-small"
|
icon="plus-small"
|
||||||
@@ -2086,7 +2086,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 py-4 px-3">
|
<div class="shrink-0 py-4">
|
||||||
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}>
|
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}>
|
||||||
{language.t("workspace.new")}
|
{language.t("workspace.new")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
|
import { type Accessor, createEffect, createMemo, For, type JSX, on, onCleanup, Show } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
||||||
@@ -101,46 +102,94 @@ const SessionRow = (props: {
|
|||||||
warmPress: () => void
|
warmPress: () => void
|
||||||
warmFocus: () => void
|
warmFocus: () => void
|
||||||
cancelHoverPrefetch: () => void
|
cancelHoverPrefetch: () => void
|
||||||
}): JSX.Element => (
|
}): JSX.Element => {
|
||||||
<A
|
const [slot, setSlot] = createStore({
|
||||||
href={`/${props.slug}/session/${props.session.id}`}
|
open: false,
|
||||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
show: false,
|
||||||
onPointerDown={props.warmPress}
|
fade: false,
|
||||||
onPointerEnter={props.warmHover}
|
})
|
||||||
onPointerLeave={props.cancelHoverPrefetch}
|
|
||||||
onFocus={props.warmFocus}
|
let f: number | undefined
|
||||||
onClick={() => {
|
const clear = () => {
|
||||||
props.setHoverSession(undefined)
|
if (f !== undefined) window.clearTimeout(f)
|
||||||
if (props.sidebarOpened()) return
|
f = undefined
|
||||||
props.clearHoverProjectSoon()
|
}
|
||||||
}}
|
|
||||||
>
|
onCleanup(clear)
|
||||||
<div class="flex items-center gap-1 w-full">
|
createEffect(
|
||||||
<div
|
on(
|
||||||
class="shrink-0 size-6 flex items-center justify-center"
|
() => props.isWorking(),
|
||||||
style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
|
(on, prev) => {
|
||||||
>
|
clear()
|
||||||
<Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
|
if (on) {
|
||||||
<Match when={props.isWorking()}>
|
setSlot({ open: true, show: true, fade: false })
|
||||||
<Spinner class="size-[15px]" />
|
return
|
||||||
</Match>
|
}
|
||||||
<Match when={props.hasPermissions()}>
|
if (prev) {
|
||||||
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
|
setSlot({ open: false, show: true, fade: true })
|
||||||
</Match>
|
f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260)
|
||||||
<Match when={props.hasError()}>
|
return
|
||||||
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
|
}
|
||||||
</Match>
|
setSlot({ open: false, show: false, fade: false })
|
||||||
<Match when={props.unseenCount() > 0}>
|
},
|
||||||
<div class="size-1.5 rounded-full bg-text-interactive-base" />
|
{ defer: true },
|
||||||
</Match>
|
),
|
||||||
</Switch>
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<A
|
||||||
|
href={`/${props.slug}/session/${props.session.id}`}
|
||||||
|
class={`relative flex items-center min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||||
|
onPointerDown={props.warmPress}
|
||||||
|
onPointerEnter={props.warmHover}
|
||||||
|
onPointerLeave={props.cancelHoverPrefetch}
|
||||||
|
onFocus={props.warmFocus}
|
||||||
|
onClick={() => {
|
||||||
|
props.setHoverSession(undefined)
|
||||||
|
if (props.sidebarOpened()) return
|
||||||
|
props.clearHoverProjectSoon()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={!props.isWorking() && (props.hasPermissions() || props.hasError() || props.unseenCount() > 0)}>
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"absolute left-0 top-1/2 -translate-y-1/2 size-1.5 rounded-full": true,
|
||||||
|
"bg-surface-warning-strong": props.hasPermissions(),
|
||||||
|
"bg-text-diff-delete-base": !props.hasPermissions() && props.hasError(),
|
||||||
|
"bg-text-interactive-base": !props.hasPermissions() && !props.hasError() && props.unseenCount() > 0,
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="flex items-center min-w-0 grow-1">
|
||||||
|
<div
|
||||||
|
class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||||
|
style={{
|
||||||
|
width: slot.open ? "16px" : "0px",
|
||||||
|
"margin-right": slot.open ? "8px" : "0px",
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Show when={slot.show}>
|
||||||
|
<div
|
||||||
|
class="transition-opacity duration-200 ease-out"
|
||||||
|
classList={{
|
||||||
|
"opacity-0": slot.fade,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spinner class="size-4" style={{ color: props.tint() ?? "var(--icon-interactive-base)" }} />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
||||||
|
{props.session.title}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
</A>
|
||||||
{props.session.title}
|
)
|
||||||
</span>
|
}
|
||||||
</div>
|
|
||||||
</A>
|
|
||||||
)
|
|
||||||
|
|
||||||
const SessionHoverPreview = (props: {
|
const SessionHoverPreview = (props: {
|
||||||
mobile?: boolean
|
mobile?: boolean
|
||||||
@@ -204,8 +253,18 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|||||||
})
|
})
|
||||||
const isWorking = createMemo(() => {
|
const isWorking = createMemo(() => {
|
||||||
if (hasPermissions()) return false
|
if (hasPermissions()) return false
|
||||||
|
const pending = (sessionStore.message[props.session.id] ?? []).findLast(
|
||||||
|
(message) =>
|
||||||
|
message.role === "assistant" &&
|
||||||
|
typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number",
|
||||||
|
)
|
||||||
const status = sessionStore.session_status[props.session.id]
|
const status = sessionStore.session_status[props.session.id]
|
||||||
return status?.type === "busy" || status?.type === "retry"
|
return (
|
||||||
|
pending !== undefined ||
|
||||||
|
status?.type === "busy" ||
|
||||||
|
status?.type === "retry" ||
|
||||||
|
(status !== undefined && status.type !== "idle")
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const tint = createMemo(() => {
|
const tint = createMemo(() => {
|
||||||
@@ -300,7 +359,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-session-id={props.session.id}
|
data-session-id={props.session.id}
|
||||||
class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3
|
class="group/session relative w-full rounded-md cursor-default pl-3 pr-3 transition-colors
|
||||||
hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ const WorkspaceSessionList = (props: {
|
|||||||
loadMore: () => Promise<void>
|
loadMore: () => Promise<void>
|
||||||
language: ReturnType<typeof useLanguage>
|
language: ReturnType<typeof useLanguage>
|
||||||
}): JSX.Element => (
|
}): JSX.Element => (
|
||||||
<nav class="flex flex-col gap-1 px-3">
|
<nav class="flex flex-col gap-1">
|
||||||
<Show when={props.showNew()}>
|
<Show when={props.showNew()}>
|
||||||
<NewSessionItem
|
<NewSessionItem
|
||||||
slug={props.slug()}
|
slug={props.slug()}
|
||||||
@@ -382,7 +382,7 @@ export const SortableWorkspace = (props: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Collapsible variant="ghost" open={open()} class="shrink-0" onOpenChange={openWrapper}>
|
<Collapsible variant="ghost" open={open()} class="shrink-0" onOpenChange={openWrapper}>
|
||||||
<div class="px-2 py-1">
|
<div class="py-1">
|
||||||
<div
|
<div
|
||||||
class="group/workspace relative"
|
class="group/workspace relative"
|
||||||
data-component="workspace-item"
|
data-component="workspace-item"
|
||||||
|
|||||||
Reference in New Issue
Block a user