Compare commits
25 Commits
dev
...
desktop-po
| Author | SHA1 | Date |
|---|---|---|
|
|
05a0df5680 | |
|
|
5fdc32c5b3 | |
|
|
d6dbdb0b40 | |
|
|
0beb8a0bf6 | |
|
|
457c246f10 | |
|
|
413f808dc0 | |
|
|
0860300660 | |
|
|
4658da0128 | |
|
|
ac255c8553 | |
|
|
e3b78fc03a | |
|
|
8abcd13e9d | |
|
|
97fd10be5d | |
|
|
ecbda741d9 | |
|
|
0296ab2cee | |
|
|
b6b3867325 | |
|
|
0983ee5b58 | |
|
|
6535556203 | |
|
|
5a6c23d484 | |
|
|
26bb424f1d | |
|
|
9f47fa7f97 | |
|
|
cffdf3320b | |
|
|
2610fec62b | |
|
|
1d8f764114 | |
|
|
ae7e07dcb4 | |
|
|
0ed60110bd |
|
|
@ -3,15 +3,20 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
import { ProjectAvatar, isValidImageFile } from "@/components/project-avatar"
|
||||
|
||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||
|
||||
function getFilename(input: string) {
|
||||
const parts = input.split("/")
|
||||
return parts[parts.length - 1] || input
|
||||
}
|
||||
|
||||
export function DialogEditProject(props: { project: LocalProject }) {
|
||||
const dialog = useDialog()
|
||||
const globalSDK = useGlobalSDK()
|
||||
|
|
@ -30,7 +35,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||
const [iconHover, setIconHover] = createSignal(false)
|
||||
|
||||
function handleFileSelect(file: File) {
|
||||
if (!file.type.startsWith("image/")) return
|
||||
if (!isValidImageFile(file)) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
setStore("iconUrl", e.target?.result as string)
|
||||
|
|
@ -98,7 +103,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||
<div class="flex gap-3 items-start">
|
||||
<div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
|
||||
<div
|
||||
class="relative size-16 rounded-md transition-colors cursor-pointer"
|
||||
class="size-16 rounded-md overflow-hidden border border-dashed transition-colors cursor-pointer"
|
||||
classList={{
|
||||
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
|
||||
"border-border-base hover:border-border-strong": !dragOver(),
|
||||
|
|
@ -115,20 +120,13 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={store.iconUrl}
|
||||
fallback={
|
||||
<div class="size-full flex items-center justify-center">
|
||||
<Avatar
|
||||
fallback={store.name || defaultName()}
|
||||
{...getAvatarColors(store.color)}
|
||||
class="size-full"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<img src={store.iconUrl} alt="Project icon" class="size-full object-cover" />
|
||||
</Show>
|
||||
<ProjectAvatar
|
||||
name={store.name || defaultName()}
|
||||
projectId={props.project.id}
|
||||
iconUrl={store.iconUrl}
|
||||
iconColor={store.color}
|
||||
class="size-full"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export const ModelSelectorPopover: Component<{
|
|||
const [open, setOpen] = createSignal(false)
|
||||
|
||||
return (
|
||||
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
|
||||
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={12}>
|
||||
<Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import { createMemo, splitProps, type ComponentProps, type JSX } from "solid-js"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
import { getAvatarColors } from "@/context/layout"
|
||||
|
||||
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
const OPENCODE_FAVICON_URL = "https://opencode.ai/favicon.svg"
|
||||
|
||||
export interface ProjectAvatarProps extends Omit<ComponentProps<"div">, "children"> {
|
||||
name: string
|
||||
iconUrl?: string
|
||||
iconColor?: string
|
||||
projectId?: string
|
||||
size?: "small" | "normal" | "large"
|
||||
}
|
||||
|
||||
export const isValidImageUrl = (url: string | undefined): boolean => {
|
||||
if (!url) {
|
||||
return false
|
||||
}
|
||||
if (url.startsWith("data:image/x-icon")) {
|
||||
return false
|
||||
}
|
||||
if (url.startsWith("data:image/vnd.microsoft.icon")) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const isValidImageFile = (file: File): boolean => {
|
||||
if (!file.type.startsWith("image/")) {
|
||||
return false
|
||||
}
|
||||
if (file.type === "image/x-icon" || file.type === "image/vnd.microsoft.icon") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const ProjectAvatar = (props: ProjectAvatarProps) => {
|
||||
const [local, rest] = splitProps(props, [
|
||||
"name",
|
||||
"iconUrl",
|
||||
"iconColor",
|
||||
"projectId",
|
||||
"size",
|
||||
"class",
|
||||
"classList",
|
||||
"style",
|
||||
])
|
||||
const colors = createMemo(() => getAvatarColors(local.iconColor))
|
||||
const validSrc = createMemo(() => {
|
||||
if (isValidImageUrl(local.iconUrl)) {
|
||||
return local.iconUrl
|
||||
}
|
||||
if (local.projectId === OPENCODE_PROJECT_ID) {
|
||||
return OPENCODE_FAVICON_URL
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
fallback={local.name}
|
||||
src={validSrc()}
|
||||
size={local.size}
|
||||
{...colors()}
|
||||
class={local.class}
|
||||
classList={local.classList}
|
||||
style={local.style as JSX.CSSProperties}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -794,7 +794,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
.abort({
|
||||
sessionID: params.id!,
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch(() => { })
|
||||
|
||||
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
|
||||
const text = prompt
|
||||
|
|
@ -1255,7 +1255,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
|
||||
const optimisticParts = requestParts.map((part) => ({
|
||||
...part,
|
||||
sessionID: session.id,
|
||||
sessionID: session?.id ?? "",
|
||||
messageID,
|
||||
})) as unknown as Part[]
|
||||
|
||||
|
|
@ -1273,9 +1273,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
const addOptimisticMessage = () => {
|
||||
setSyncStore(
|
||||
produce((draft) => {
|
||||
const messages = draft.message[session.id]
|
||||
const messages = draft.message[session?.id ?? ""]
|
||||
if (!messages) {
|
||||
draft.message[session.id] = [optimisticMessage]
|
||||
draft.message[session?.id ?? ""] = [optimisticMessage]
|
||||
} else {
|
||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||
messages.splice(result.index, 0, optimisticMessage)
|
||||
|
|
@ -1291,7 +1291,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
const removeOptimisticMessage = () => {
|
||||
setSyncStore(
|
||||
produce((draft) => {
|
||||
const messages = draft.message[session.id]
|
||||
const messages = draft.message[session?.id ?? ""]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
|
|
@ -1567,7 +1567,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
</Show>
|
||||
</div>
|
||||
<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>
|
||||
<Match when={store.mode === "shell"}>
|
||||
<div class="flex items-center gap-2 px-2 h-6">
|
||||
|
|
@ -1618,13 +1618,60 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
title="Thinking effort"
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
|
||||
onClick={() => local.model.variant.cycle()}
|
||||
>
|
||||
{local.model.variant.current() ?? "Default"}
|
||||
</Button>
|
||||
{(() => {
|
||||
const [text, setText] = createSignal(local.model.variant.current() ?? "Default")
|
||||
const [animating, setAnimating] = createSignal(false)
|
||||
let locked = false
|
||||
|
||||
const handleClick = async () => {
|
||||
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>
|
||||
</Show>
|
||||
<Show when={permission.permissionsEnabled() && params.id}>
|
||||
|
|
@ -1700,7 +1747,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
disabled={!prompt.dirty() && !working()}
|
||||
icon={working() ? "stop" : "arrow-up"}
|
||||
variant="primary"
|
||||
class="h-6 w-4.5"
|
||||
class="h-6 w-6"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ export function SessionHeader() {
|
|||
<Show when={shareEnabled() && currentSession()}>
|
||||
<div class="flex items-center">
|
||||
<Popover
|
||||
gutter={16}
|
||||
title="Publish on web"
|
||||
description={
|
||||
shareUrl()
|
||||
|
|
@ -298,7 +299,7 @@ export function SessionHeader() {
|
|||
</div>
|
||||
</Popover>
|
||||
<Show when={shareUrl()} fallback={<div class="size-6" aria-hidden="true" />}>
|
||||
<Tooltip value={state.copied ? "Copied" : "Copy link"} placement="top" gutter={8}>
|
||||
<Tooltip value={state.copied ? "Copied" : "Copy link"} placement="top-end" gutter={12}>
|
||||
<IconButton
|
||||
icon={state.copied ? "check" : "copy"}
|
||||
variant="secondary"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
|
|||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
|
|
@ -35,7 +34,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
|||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
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 { createStore, produce, reconcile } from "solid-js/store"
|
||||
import {
|
||||
|
|
@ -60,12 +59,14 @@ import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
|||
import { DialogSelectProvider } from "@/components/dialog-select-provider"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { useCommand, type CommandOption } from "@/context/command"
|
||||
import { ProjectAvatar } from "@/components/project-avatar"
|
||||
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
||||
import { navStart } from "@/utils/perf"
|
||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
import { DialogEditProject } from "@/components/dialog-edit-project"
|
||||
import { Titlebar } from "@/components/titlebar"
|
||||
import { useServer } from "@/context/server"
|
||||
import { ScrollReveal } from "@opencode-ai/ui/scroll-reveal"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const [store, setStore, , ready] = persisted(
|
||||
|
|
@ -187,7 +188,9 @@ export default function Layout(props: ParentProps) {
|
|||
onClick={stopPropagation}
|
||||
onTouchStart={stopPropagation}
|
||||
>
|
||||
{props.value()}
|
||||
<ScrollReveal>
|
||||
{props.value()}
|
||||
</ScrollReveal>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
|
|
@ -1277,15 +1280,16 @@ export default function Layout(props: ParentProps) {
|
|||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||
const mask = "radial-gradient(circle 5px at calc(100% - 4px) 4px, transparent 5px, black 5.5px)"
|
||||
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
|
||||
return (
|
||||
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>
|
||||
<div class="size-full rounded overflow-clip">
|
||||
<Avatar
|
||||
fallback={name()}
|
||||
src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.override}
|
||||
{...getAvatarColors(props.project.icon?.color)}
|
||||
<div class={`relative size-8 shrink-0 rounded-sm ${props.class ?? ""}`}>
|
||||
<div class="size-full rounded-sm overflow-clip">
|
||||
<ProjectAvatar
|
||||
name={name()}
|
||||
projectId={props.project.id}
|
||||
iconUrl={props.project.icon?.url}
|
||||
iconColor={props.project.icon?.color}
|
||||
size="small"
|
||||
class="size-full rounded"
|
||||
style={
|
||||
notifications().length > 0 && props.notify
|
||||
|
|
@ -1348,7 +1352,7 @@ export default function Layout(props: ParentProps) {
|
|||
})
|
||||
|
||||
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 hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened())
|
||||
|
|
@ -1421,7 +1425,7 @@ export default function Layout(props: ParentProps) {
|
|||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<HoverCard openDelay={150} closeDelay={100} placement="right-start" gutter={16} 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>}>
|
||||
<MessageNav
|
||||
messages={hoverMessages() ?? []}
|
||||
|
|
@ -1539,7 +1543,7 @@ export default function Layout(props: ParentProps) {
|
|||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||
<div use: sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||
<Collapsible variant="ghost" open={open()} class="shrink-0" onOpenChange={openWrapper}>
|
||||
<div class="px-2 py-1">
|
||||
<div class="group/workspace relative">
|
||||
|
|
@ -1653,7 +1657,7 @@ export default function Layout(props: ParentProps) {
|
|||
size="large"
|
||||
onClick={(e: MouseEvent) => {
|
||||
loadMore()
|
||||
;(e.currentTarget as HTMLButtonElement).blur()
|
||||
; (e.currentTarget as HTMLButtonElement).blur()
|
||||
}}
|
||||
>
|
||||
Load more
|
||||
|
|
@ -1721,7 +1725,7 @@ export default function Layout(props: ParentProps) {
|
|||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||
<div use: sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||
<HoverCard
|
||||
openDelay={0}
|
||||
closeDelay={0}
|
||||
|
|
@ -1819,7 +1823,7 @@ export default function Layout(props: ParentProps) {
|
|||
class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar"
|
||||
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()}>
|
||||
<SessionSkeleton />
|
||||
</Show>
|
||||
|
|
@ -1834,7 +1838,7 @@ export default function Layout(props: ParentProps) {
|
|||
size="large"
|
||||
onClick={(e: MouseEvent) => {
|
||||
loadMore()
|
||||
;(e.currentTarget as HTMLButtonElement).blur()
|
||||
; (e.currentTarget as HTMLButtonElement).blur()
|
||||
}}
|
||||
>
|
||||
Load more
|
||||
|
|
|
|||
|
|
@ -570,7 +570,7 @@ export default function Page() {
|
|||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
if (status()?.type !== "idle") {
|
||||
await sdk.client.session.abort({ sessionID }).catch(() => {})
|
||||
await sdk.client.session.abort({ sessionID }).catch(() => { })
|
||||
}
|
||||
const revert = info()?.revert?.messageID
|
||||
// Find the last user message that's not already reverted
|
||||
|
|
@ -653,69 +653,69 @@ export default function Page() {
|
|||
},
|
||||
...(sync.data.config.share !== "disabled"
|
||||
? [
|
||||
{
|
||||
id: "session.share",
|
||||
title: "Share session",
|
||||
description: "Share this session and copy the URL to clipboard",
|
||||
category: "Session",
|
||||
slash: "share",
|
||||
disabled: !params.id || !!info()?.share?.url,
|
||||
onSelect: async () => {
|
||||
if (!params.id) return
|
||||
await sdk.client.session
|
||||
.share({ sessionID: params.id })
|
||||
.then((res) => {
|
||||
navigator.clipboard.writeText(res.data!.share!.url).catch(() =>
|
||||
showToast({
|
||||
title: "Failed to copy URL to clipboard",
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
id: "session.share",
|
||||
title: "Share session",
|
||||
description: "Share this session and copy the URL to clipboard",
|
||||
category: "Session",
|
||||
slash: "share",
|
||||
disabled: !params.id || !!info()?.share?.url,
|
||||
onSelect: async () => {
|
||||
if (!params.id) return
|
||||
await sdk.client.session
|
||||
.share({ sessionID: params.id })
|
||||
.then((res) => {
|
||||
navigator.clipboard.writeText(res.data!.share!.url).catch(() =>
|
||||
showToast({
|
||||
title: "Session shared",
|
||||
description: "Share URL copied to clipboard!",
|
||||
variant: "success",
|
||||
}),
|
||||
)
|
||||
.catch(() =>
|
||||
showToast({
|
||||
title: "Failed to share session",
|
||||
description: "An error occurred while sharing the session",
|
||||
title: "Failed to copy URL to clipboard",
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
.then(() =>
|
||||
showToast({
|
||||
title: "Session shared",
|
||||
description: "Share URL copied to clipboard!",
|
||||
variant: "success",
|
||||
}),
|
||||
)
|
||||
.catch(() =>
|
||||
showToast({
|
||||
title: "Failed to share session",
|
||||
description: "An error occurred while sharing the session",
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "session.unshare",
|
||||
title: "Unshare session",
|
||||
description: "Stop sharing this session",
|
||||
category: "Session",
|
||||
slash: "unshare",
|
||||
disabled: !params.id || !info()?.share?.url,
|
||||
onSelect: async () => {
|
||||
if (!params.id) return
|
||||
await sdk.client.session
|
||||
.unshare({ sessionID: params.id })
|
||||
.then(() =>
|
||||
showToast({
|
||||
title: "Session unshared",
|
||||
description: "Session unshared successfully!",
|
||||
variant: "success",
|
||||
}),
|
||||
)
|
||||
.catch(() =>
|
||||
showToast({
|
||||
title: "Failed to unshare session",
|
||||
description: "An error occurred while unsharing the session",
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "session.unshare",
|
||||
title: "Unshare session",
|
||||
description: "Stop sharing this session",
|
||||
category: "Session",
|
||||
slash: "unshare",
|
||||
disabled: !params.id || !info()?.share?.url,
|
||||
onSelect: async () => {
|
||||
if (!params.id) return
|
||||
await sdk.client.session
|
||||
.unshare({ sessionID: params.id })
|
||||
.then(() =>
|
||||
showToast({
|
||||
title: "Session unshared",
|
||||
description: "Session unshared successfully!",
|
||||
variant: "success",
|
||||
}),
|
||||
)
|
||||
.catch(() =>
|
||||
showToast({
|
||||
title: "Failed to unshare session",
|
||||
description: "An error occurred while unsharing the session",
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
: []),
|
||||
])
|
||||
|
||||
|
|
@ -1265,7 +1265,7 @@ export default function Page() {
|
|||
if (isDesktop()) scheduleScrollSpy(e.currentTarget)
|
||||
}}
|
||||
onClick={autoScroll.handleInteraction}
|
||||
class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar"
|
||||
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" }}
|
||||
>
|
||||
<Show when={info()?.title}>
|
||||
|
|
@ -1332,13 +1332,12 @@ export default function Page() {
|
|||
navMark({ dir: params.dir, to: id, name: "session:first-turn-mounted" })
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={anchor(message.id)}
|
||||
data-message-id={message.id}
|
||||
classList={{
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"min-w-0 w-full max-w-full snap-both": true,
|
||||
"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)]":
|
||||
platform.platform !== "desktop",
|
||||
|
|
|
|||
|
|
@ -9,11 +9,15 @@
|
|||
user-select: none;
|
||||
cursor: default;
|
||||
outline: none;
|
||||
padding: 4px 8px;
|
||||
white-space: nowrap;
|
||||
transition-property: background-color, border-color, color, box-shadow;
|
||||
transition-duration: 200ms;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
|
||||
|
||||
&[data-variant="primary"] {
|
||||
background-color: var(--icon-strong-base);
|
||||
border-color: var(--border-weak-base);
|
||||
border-color: var(--border-weaker-base);
|
||||
color: var(--icon-invert-base);
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
|
|
@ -102,19 +106,15 @@
|
|||
}
|
||||
|
||||
&[data-size="small"] {
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
padding: 2px 10px;
|
||||
&[data-icon] {
|
||||
padding: 0 12px 0 4px;
|
||||
}
|
||||
|
||||
font-size: var(--font-size-small);
|
||||
line-height: var(--line-height-large);
|
||||
gap: 4px;
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
|
|
@ -163,3 +163,39 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@
|
|||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 12px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
|
|
|
|||
|
|
@ -2,26 +2,24 @@
|
|||
[data-component="dropdown-menu-sub-content"] {
|
||||
min-width: 8rem;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
background-clip: padding-box;
|
||||
background-color: var(--surface-raised-stronger-non-alpha);
|
||||
padding: 4px;
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 50;
|
||||
z-index: 100;
|
||||
transform-origin: var(--kb-menu-content-transform-origin);
|
||||
|
||||
animation: dropdownMenuContentHide 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[data-closed] {
|
||||
animation: dropdown-menu-close 0.15s ease-out;
|
||||
@starting-style {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
animation: dropdown-menu-open 0.15s ease-out;
|
||||
pointer-events: auto;
|
||||
animation: dropdownMenuContentShow 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,18 +36,23 @@
|
|||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-strong);
|
||||
|
||||
&[data-highlighted] {
|
||||
background: var(--surface-raised-base-hover);
|
||||
transition:
|
||||
background-color 200ms cubic-bezier(0.25, 0, 0.5, 1),
|
||||
color 200ms cubic-bezier(0.25, 0, 0.5, 1);
|
||||
outline: none;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
|
|
@ -61,6 +64,8 @@
|
|||
[data-slot="dropdown-menu-sub-trigger"] {
|
||||
&[data-expanded] {
|
||||
background: var(--surface-raised-base-hover);
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,24 +107,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes dropdown-menu-open {
|
||||
@keyframes dropdownMenuContentShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
transform: scaleY(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdown-menu-close {
|
||||
@keyframes dropdownMenuContentHide {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transform: scaleY(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
transform: scaleY(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,6 @@
|
|||
|
||||
&[data-variant="ghost"] {
|
||||
background-color: transparent;
|
||||
/* color: var(--icon-base); */
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
color: var(--icon-base);
|
||||
|
|
@ -85,25 +84,41 @@
|
|||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
|
||||
/* [data-slot="icon-svg"] { */
|
||||
/* color: var(--icon-hover); */
|
||||
/* } */
|
||||
}
|
||||
&:focus:not(:disabled) {
|
||||
background-color: var(--surface-focus);
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
/* [data-slot="icon-svg"] { */
|
||||
/* color: var(--icon-active); */
|
||||
/* } */
|
||||
}
|
||||
&:selected:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
/* [data-slot="icon-svg"] { */
|
||||
/* color: var(--icon-selected); */
|
||||
/* } */
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant="weak"] {
|
||||
opacity: 0.8;
|
||||
background-color: transparent;
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
color: var(--icon-base);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: 1;
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
&:focus:not(:disabled) {
|
||||
opacity: 1;
|
||||
background-color: var(--surface-focus);
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
opacity: 1;
|
||||
background-color: var(--surface-raised-base-active);
|
||||
}
|
||||
&:selected:not(:disabled) {
|
||||
opacity: 1;
|
||||
background-color: var(--surface-raised-base-active);
|
||||
}
|
||||
&:disabled {
|
||||
color: var(--icon-invert-base);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export interface IconButtonProps extends ComponentProps<typeof Kobalte> {
|
|||
icon: IconProps["name"]
|
||||
size?: "normal" | "large"
|
||||
iconSize?: IconProps["size"]
|
||||
variant?: "primary" | "secondary" | "ghost"
|
||||
variant?: "primary" | "secondary" | "ghost" | "weak"
|
||||
}
|
||||
|
||||
export function IconButton(props: ComponentProps<"button"> & IconButtonProps) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const icons = {
|
|||
"bubble-5": `<path d="M18.3327 9.99935C18.3327 5.57227 15.0919 2.91602 9.99935 2.91602C4.90676 2.91602 1.66602 5.57227 1.66602 9.99935C1.66602 11.1487 2.45505 13.1006 2.57637 13.3939C2.58707 13.4197 2.59766 13.4434 2.60729 13.4697C2.69121 13.6987 3.04209 14.9354 1.66602 16.7674C3.51787 17.6528 5.48453 16.1973 5.48453 16.1973C6.84518 16.9193 8.46417 17.0827 9.99935 17.0827C15.0919 17.0827 18.3327 14.4264 18.3327 9.99935Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
brain: `<path d="M13.332 8.7487C11.4911 8.7487 9.9987 7.25631 9.9987 5.41536M6.66536 11.2487C8.50631 11.2487 9.9987 12.7411 9.9987 14.582M9.9987 2.78209L9.9987 17.0658M16.004 15.0475C17.1255 14.5876 17.9154 13.4849 17.9154 12.1978C17.9154 11.3363 17.5615 10.5575 16.9913 9.9987C17.5615 9.43991 17.9154 8.66108 17.9154 7.79962C17.9154 6.21199 16.7136 4.90504 15.1702 4.73878C14.7858 3.21216 13.4039 2.08203 11.758 2.08203C11.1171 2.08203 10.5162 2.25337 9.9987 2.55275C9.48117 2.25337 8.88032 2.08203 8.23944 2.08203C6.59353 2.08203 5.21157 3.21216 4.82722 4.73878C3.28377 4.90504 2.08203 6.21199 2.08203 7.79962C2.08203 8.66108 2.43585 9.43991 3.00609 9.9987C2.43585 10.5575 2.08203 11.3363 2.08203 12.1978C2.08203 13.4849 2.87191 14.5876 3.99339 15.0475C4.46688 16.7033 5.9917 17.9154 7.79962 17.9154C8.61335 17.9154 9.36972 17.6698 9.9987 17.2488C10.6277 17.6698 11.384 17.9154 12.1978 17.9154C14.0057 17.9154 15.5305 16.7033 16.004 15.0475Z" stroke="currentColor"/>`,
|
||||
"bullet-list": `<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square" stroke-width="1.25"/>`,
|
||||
"chevron-down": `<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"chevron-right": `<path d="M8.33301 13.3327L11.6663 9.99935L8.33301 6.66602" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"chevron-grabber-vertical": `<path d="M6.66675 12.4998L10.0001 15.8332L13.3334 12.4998M6.66675 7.49984L10.0001 4.1665L13.3334 7.49984" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
|
|
@ -33,7 +33,7 @@ const icons = {
|
|||
mcp: `<g><path d="M0.972656 9.37176L9.5214 1.60019C10.7018 0.527151 12.6155 0.527151 13.7957 1.60019C14.9761 2.67321 14.9761 4.41295 13.7957 5.48599L7.3397 11.3552" stroke="currentColor" stroke-linecap="round"/><path d="M7.42871 11.2747L13.7957 5.48643C14.9761 4.41338 16.8898 4.41338 18.0702 5.48643L18.1147 5.52688C19.2951 6.59993 19.2951 8.33966 18.1147 9.4127L10.3831 16.4414C9.98966 16.7991 9.98966 17.379 10.3831 17.7366L11.9707 19.1799" stroke="currentColor" stroke-linecap="round"/><path d="M11.6587 3.54346L5.33619 9.29119C4.15584 10.3642 4.15584 12.1039 5.33619 13.177C6.51649 14.25 8.43019 14.25 9.61054 13.177L15.9331 7.42923" stroke="currentColor" stroke-linecap="round"/></g>`,
|
||||
glasses: `<path d="M0.416626 7.91667H1.66663M19.5833 7.91667H18.3333M11.866 7.57987C11.3165 7.26398 10.6793 7.08333 9.99996 7.08333C9.32061 7.08333 8.68344 7.26398 8.13389 7.57987M8.74996 10C8.74996 12.0711 7.07103 13.75 4.99996 13.75C2.92889 13.75 1.24996 12.0711 1.24996 10C1.24996 7.92893 2.92889 6.25 4.99996 6.25C7.07103 6.25 8.74996 7.92893 8.74996 10ZM18.75 10C18.75 12.0711 17.071 13.75 15 13.75C12.9289 13.75 11.25 12.0711 11.25 10C11.25 7.92893 12.9289 6.25 15 6.25C17.071 6.25 18.75 7.92893 18.75 10Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"magnifying-glass-menu": `<path d="M2.08325 10.0002H4.58325M2.08325 5.41683H5.41659M2.08325 14.5835H5.41659M16.4583 13.9585L18.7499 16.2502M17.9166 10.0002C17.9166 12.9917 15.4915 15.4168 12.4999 15.4168C9.50838 15.4168 7.08325 12.9917 7.08325 10.0002C7.08325 7.00862 9.50838 4.5835 12.4999 4.5835C15.4915 4.5835 17.9166 7.00862 17.9166 10.0002Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"window-cursor": `<path d="M17.9166 10.4167V3.75H2.08325V17.0833H10.4166M17.9166 13.5897L11.6666 11.6667L13.5897 17.9167L15.032 15.0321L17.9166 13.5897Z" stroke="currentColor" stroke-width="1.07143" stroke-linecap="square"/><path d="M5.00024 6.125C5.29925 6.12518 5.54126 6.36795 5.54126 6.66699C5.54108 6.96589 5.29914 7.20783 5.00024 7.20801C4.7012 7.20801 4.45843 6.966 4.45825 6.66699C4.45825 6.36784 4.70109 6.125 5.00024 6.125ZM7.91626 6.125C8.21541 6.125 8.45825 6.36784 8.45825 6.66699C8.45808 6.966 8.21531 7.20801 7.91626 7.20801C7.61736 7.20783 7.37542 6.96589 7.37524 6.66699C7.37524 6.36795 7.61726 6.12518 7.91626 6.125ZM10.8333 6.125C11.1324 6.125 11.3752 6.36784 11.3752 6.66699C11.3751 6.966 11.1323 7.20801 10.8333 7.20801C10.5342 7.20801 10.2914 6.966 10.2913 6.66699C10.2913 6.36784 10.5341 6.125 10.8333 6.125Z" fill="currentColor" stroke="currentColor" stroke-width="0.25" stroke-linecap="square"/>`,
|
||||
"window-cursor": `<path d="M17.9166 10.4167V3.75H2.08325V17.0833H10.4166M17.9166 13.5897L11.6666 11.6667L13.5897 17.9167L15.032 15.0321L17.9166 13.5897Z" stroke="currentColor" stroke-width="1.25" stroke-linecap="square"/><path d="M5.00024 6.125C5.29925 6.12518 5.54126 6.36795 5.54126 6.66699C5.54108 6.96589 5.29914 7.20783 5.00024 7.20801C4.7012 7.20801 4.45843 6.966 4.45825 6.66699C4.45825 6.36784 4.70109 6.125 5.00024 6.125ZM7.91626 6.125C8.21541 6.125 8.45825 6.36784 8.45825 6.66699C8.45808 6.966 8.21531 7.20801 7.91626 7.20801C7.61736 7.20783 7.37542 6.96589 7.37524 6.66699C7.37524 6.36795 7.61726 6.12518 7.91626 6.125ZM10.8333 6.125C11.1324 6.125 11.3752 6.36784 11.3752 6.66699C11.3751 6.966 11.1323 7.20801 10.8333 7.20801C10.5342 7.20801 10.2914 6.966 10.2913 6.66699C10.2913 6.36784 10.5341 6.125 10.8333 6.125Z" fill="currentColor" stroke="currentColor" stroke-width="0.25" stroke-linecap="square"/>`,
|
||||
task: `<path d="M9.99992 2.0835V17.9168M7.08325 3.75016H2.08325V16.2502H7.08325M12.9166 16.2502H17.9166V3.75016H12.9166" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
stop: `<rect x="5" y="5" width="10" height="10" fill="currentColor"/>`,
|
||||
"layout-left": `<path d="M2.91675 2.91699L2.91675 2.41699L2.41675 2.41699L2.41675 2.91699L2.91675 2.91699ZM17.0834 2.91699L17.5834 2.91699L17.5834 2.41699L17.0834 2.41699L17.0834 2.91699ZM17.0834 17.0837L17.0834 17.5837L17.5834 17.5837L17.5834 17.0837L17.0834 17.0837ZM2.91675 17.0837L2.41675 17.0837L2.41675 17.5837L2.91675 17.5837L2.91675 17.0837ZM7.41674 17.0837L7.41674 17.5837L8.41674 17.5837L8.41674 17.0837L7.91674 17.0837L7.41674 17.0837ZM8.41674 2.91699L8.41674 2.41699L7.41674 2.41699L7.41674 2.91699L7.91674 2.91699L8.41674 2.91699ZM2.91675 2.91699L2.91675 3.41699L17.0834 3.41699L17.0834 2.91699L17.0834 2.41699L2.91675 2.41699L2.91675 2.91699ZM17.0834 2.91699L16.5834 2.91699L16.5834 17.0837L17.0834 17.0837L17.5834 17.0837L17.5834 2.91699L17.0834 2.91699ZM17.0834 17.0837L17.0834 16.5837L2.91675 16.5837L2.91675 17.0837L2.91675 17.5837L17.0834 17.5837L17.0834 17.0837ZM2.91675 17.0837L3.41675 17.0837L3.41675 2.91699L2.91675 2.91699L2.41675 2.91699L2.41675 17.0837L2.91675 17.0837ZM7.91674 17.0837L8.41674 17.0837L8.41674 2.91699L7.91674 2.91699L7.41674 2.91699L7.41674 17.0837L7.91674 17.0837Z" fill="currentColor"/>`,
|
||||
|
|
@ -53,7 +53,7 @@ const icons = {
|
|||
"dot-grid": `<path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" fill="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" fill="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" fill="currentColor"/><path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" stroke="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" stroke="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" stroke="currentColor"/>`,
|
||||
"circle-check": `<path d="M12.4987 7.91732L8.7487 12.5007L7.08203 10.834M17.9154 10.0007C17.9154 14.3729 14.371 17.9173 9.9987 17.9173C5.62644 17.9173 2.08203 14.3729 2.08203 10.0007C2.08203 5.6284 5.62644 2.08398 9.9987 2.08398C14.371 2.08398 17.9154 5.6284 17.9154 10.0007Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
copy: `<path d="M6.2513 6.24935V2.91602H17.0846V13.7493H13.7513M13.7513 6.24935V17.0827H2.91797V6.24935H13.7513Z" stroke="currentColor" stroke-linecap="round"/>`,
|
||||
check: `<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
check: `<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square" stroke-width="1.25" />`,
|
||||
photo: `<path d="M16.6665 16.6666L11.6665 11.6666L9.99984 13.3333L6.6665 9.99996L3.08317 13.5833M2.9165 2.91663H17.0832V17.0833H2.9165V2.91663ZM13.3332 7.49996C13.3332 8.30537 12.6803 8.95829 11.8748 8.95829C11.0694 8.95829 10.4165 8.30537 10.4165 7.49996C10.4165 6.69454 11.0694 6.04163 11.8748 6.04163C12.6803 6.04163 13.3332 6.69454 13.3332 7.49996Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
share: `<path d="M10.0013 12.0846L10.0013 3.33464M13.7513 6.66797L10.0013 2.91797L6.2513 6.66797M17.0846 10.418V17.0846H2.91797V10.418" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
download: `<path d="M13.9583 10.6257L10 14.584L6.04167 10.6257M10 2.08398V13.959M16.25 17.9173H3.75" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
[data-component="list"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
padding: 0 12px;
|
||||
|
||||
|
|
@ -82,12 +82,9 @@
|
|||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
mask: linear-gradient(to bottom, #ffff calc(100% - var(--bottom-fade)), #0000);
|
||||
animation: scroll;
|
||||
animation-timeline: --scroll;
|
||||
scroll-timeline: --scroll y;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { createEffect, createSignal, For, onCleanup, type JSX, on, Show } from "
|
|||
import { createStore } from "solid-js/store"
|
||||
import { Icon, type IconProps } from "./icon"
|
||||
import { IconButton } from "./icon-button"
|
||||
import { ScrollFade } from "./scroll-fade"
|
||||
import { TextField } from "./text-field"
|
||||
|
||||
export interface ListSearchProps {
|
||||
|
|
@ -197,11 +198,17 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||
/>
|
||||
</div>
|
||||
<Show when={internalFilter()}>
|
||||
<IconButton icon="circle-x" variant="ghost" onClick={() => setInternalFilter("")} />
|
||||
<IconButton icon="circle-x" variant="weak" onClick={() => setInternalFilter("")} />
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<div ref={setScrollRef} data-slot="list-scroll">
|
||||
<ScrollFade
|
||||
ref={setScrollRef}
|
||||
direction="vertical"
|
||||
fadeStartSize={0}
|
||||
fadeEndSize={20}
|
||||
data-slot="list-scroll"
|
||||
>
|
||||
<Show
|
||||
when={flat().length > 0}
|
||||
fallback={
|
||||
|
|
@ -260,7 +267,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</ScrollFade>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
padding-left: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
&[data-size="normal"] {
|
||||
width: 240px;
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&[data-size="compact"] {
|
||||
|
|
@ -36,6 +37,7 @@
|
|||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&[data-active] [data-slot="message-nav-tick-line"] {
|
||||
background-color: var(--icon-strong-base);
|
||||
|
|
@ -54,7 +56,8 @@
|
|||
|
||||
[data-slot="message-nav-tick-button"]:hover [data-slot="message-nav-tick-line"] {
|
||||
width: 100%;
|
||||
background-color: var(--icon-strong-base);
|
||||
color: var(--text-strong);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
[data-slot="message-nav-message-button"] {
|
||||
|
|
@ -62,11 +65,12 @@
|
|||
align-items: center;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
color: inherit;
|
||||
column-gap: 12px;
|
||||
cursor: default;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 4px 12px;
|
||||
padding: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
|
|
@ -79,16 +83,21 @@
|
|||
min-width: 0;
|
||||
text-align: left;
|
||||
|
||||
&:not(:hover) {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&[data-active] {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
}
|
||||
|
||||
[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"] {
|
||||
background-color: var(--surface-base-active);
|
||||
color: var(--text-base);
|
||||
}
|
||||
|
||||
[data-slot="message-nav-item"]:active [data-slot="message-nav-title-preview"] {
|
||||
|
|
@ -101,7 +110,7 @@
|
|||
|
||||
[data-slot="message-nav-tooltip-content"] {
|
||||
display: flex;
|
||||
padding: 4px 4px 6px 4px;
|
||||
padding: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: var(--radius-md);
|
||||
|
|
@ -119,4 +128,4 @@
|
|||
* {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { UserMessage } from "@opencode-ai/sdk/v2"
|
|||
import { ComponentProps, For, Match, Show, splitProps, Switch } from "solid-js"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { Tooltip } from "@kobalte/core/tooltip"
|
||||
import { ScrollReveal } from "./scroll-reveal"
|
||||
|
||||
export function MessageNav(
|
||||
props: ComponentProps<"ul"> & {
|
||||
|
|
@ -43,14 +44,16 @@ export function MessageNav(
|
|||
</Match>
|
||||
<Match when={local.size === "normal"}>
|
||||
<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
|
||||
data-slot="message-nav-title-preview"
|
||||
data-active={message.id === local.current?.id || undefined}
|
||||
>
|
||||
<Show when={local.getLabel?.(message) ?? message.summary?.title} fallback="New message">
|
||||
{local.getLabel?.(message) ?? message.summary?.title}
|
||||
</Show>
|
||||
<ScrollReveal fadeEndSize={12}>
|
||||
<Show when={local.getLabel?.(message) ?? message.summary?.title} fallback="New message">
|
||||
{local.getLabel?.(message) ?? message.summary?.title}
|
||||
</Show>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</button>
|
||||
</Match>
|
||||
|
|
@ -79,4 +82,4 @@ export function MessageNav(
|
|||
<Match when={local.size === "normal"}>{content()}</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,14 @@
|
|||
|
||||
transform-origin: var(--kb-popover-content-transform-origin);
|
||||
|
||||
[data-origin-top-right] {
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
[data-origin-top-left] {
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
[data-component="scroll-fade"] {
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: none;
|
||||
box-sizing: border-box;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&[data-direction="horizontal"] {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
/* Both fades */
|
||||
&[data-fade-start][data-fade-end] {
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
black var(--scroll-fade-start),
|
||||
black calc(100% - var(--scroll-fade-end)),
|
||||
transparent
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
black var(--scroll-fade-start),
|
||||
black calc(100% - var(--scroll-fade-end)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
/* Only start fade */
|
||||
&[data-fade-start]:not([data-fade-end]) {
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
black var(--scroll-fade-start),
|
||||
black 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
black var(--scroll-fade-start),
|
||||
black 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Only end fade */
|
||||
&:not([data-fade-start])[data-fade-end] {
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
black 0%,
|
||||
black calc(100% - var(--scroll-fade-end)),
|
||||
transparent
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to right,
|
||||
black 0%,
|
||||
black calc(100% - var(--scroll-fade-end)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-direction="vertical"] {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
/* Both fades */
|
||||
&[data-fade-start][data-fade-end] {
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
black var(--scroll-fade-start),
|
||||
black calc(100% - var(--scroll-fade-end)),
|
||||
transparent
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
black var(--scroll-fade-start),
|
||||
black calc(100% - var(--scroll-fade-end)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
/* Only start fade */
|
||||
&[data-fade-start]:not([data-fade-end]) {
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
black var(--scroll-fade-start),
|
||||
black 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
black var(--scroll-fade-start),
|
||||
black 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Only end fade */
|
||||
&:not([data-fade-start])[data-fade-end] {
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
black 0%,
|
||||
black calc(100% - var(--scroll-fade-end)),
|
||||
transparent
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
black 0%,
|
||||
black calc(100% - var(--scroll-fade-end)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
import {
|
||||
type JSX,
|
||||
createEffect,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
onMount,
|
||||
splitProps,
|
||||
} from "solid-js"
|
||||
import "./scroll-fade.css"
|
||||
|
||||
export interface ScrollFadeProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
direction?: "horizontal" | "vertical"
|
||||
fadeStartSize?: number
|
||||
fadeEndSize?: number
|
||||
trackTransformSelector?: string
|
||||
ref?: (el: HTMLDivElement) => void
|
||||
}
|
||||
|
||||
export function ScrollFade(props: ScrollFadeProps) {
|
||||
const [local, others] = splitProps(props, [
|
||||
"children",
|
||||
"direction",
|
||||
"fadeStartSize",
|
||||
"fadeEndSize",
|
||||
"trackTransformSelector",
|
||||
"class",
|
||||
"style",
|
||||
"ref",
|
||||
])
|
||||
|
||||
const direction = () => local.direction ?? "vertical"
|
||||
const fadeStartSize = () => local.fadeStartSize ?? 20
|
||||
const fadeEndSize = () => local.fadeEndSize ?? 20
|
||||
|
||||
const getTransformOffset = (element: Element): number => {
|
||||
const style = getComputedStyle(element)
|
||||
const transform = style.transform
|
||||
if (!transform || transform === "none") return 0
|
||||
|
||||
const match = transform.match(/matrix(?:3d)?\(([^)]+)\)/)
|
||||
if (!match) return 0
|
||||
|
||||
const values = match[1].split(",").map((v) => parseFloat(v.trim()))
|
||||
const isHorizontal = direction() === "horizontal"
|
||||
|
||||
if (transform.startsWith("matrix3d")) {
|
||||
return isHorizontal ? -(values[12] || 0) : -(values[13] || 0)
|
||||
} else {
|
||||
return isHorizontal ? -(values[4] || 0) : -(values[5] || 0)
|
||||
}
|
||||
}
|
||||
|
||||
let containerRef: HTMLDivElement | undefined
|
||||
|
||||
const [fadeStart, setFadeStart] = createSignal(0)
|
||||
const [fadeEnd, setFadeEnd] = createSignal(0)
|
||||
const [isScrollable, setIsScrollable] = createSignal(false)
|
||||
|
||||
let lastScrollPos = 0
|
||||
let lastTransformPos = 0
|
||||
let lastScrollSize = 0
|
||||
let lastClientSize = 0
|
||||
|
||||
const updateFade = () => {
|
||||
if (!containerRef) return
|
||||
|
||||
const isHorizontal = direction() === "horizontal"
|
||||
const scrollPos = isHorizontal ? containerRef.scrollLeft : containerRef.scrollTop
|
||||
const scrollSize = isHorizontal ? containerRef.scrollWidth : containerRef.scrollHeight
|
||||
const clientSize = isHorizontal ? containerRef.clientWidth : containerRef.clientHeight
|
||||
|
||||
let transformPos = 0
|
||||
if (local.trackTransformSelector) {
|
||||
const transformElement = containerRef.querySelector(local.trackTransformSelector)
|
||||
if (transformElement) {
|
||||
transformPos = getTransformOffset(transformElement)
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveScrollPos = Math.max(scrollPos, transformPos)
|
||||
|
||||
if (
|
||||
effectiveScrollPos === lastScrollPos &&
|
||||
transformPos === lastTransformPos &&
|
||||
scrollSize === lastScrollSize &&
|
||||
clientSize === lastClientSize
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
lastScrollPos = effectiveScrollPos
|
||||
lastTransformPos = transformPos
|
||||
lastScrollSize = scrollSize
|
||||
lastClientSize = clientSize
|
||||
|
||||
const maxScroll = scrollSize - clientSize
|
||||
const canScroll = maxScroll > 1
|
||||
|
||||
setIsScrollable(canScroll)
|
||||
|
||||
if (!canScroll) {
|
||||
setFadeStart(0)
|
||||
setFadeEnd(0)
|
||||
return
|
||||
}
|
||||
|
||||
const progress = maxScroll > 0 ? effectiveScrollPos / maxScroll : 0
|
||||
|
||||
const startProgress = Math.min(progress / 0.1, 1)
|
||||
setFadeStart(startProgress * fadeStartSize())
|
||||
|
||||
const endProgress = progress > 0.9 ? (1 - progress) / 0.1 : 1
|
||||
setFadeEnd(Math.max(0, endProgress) * fadeEndSize())
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!containerRef) return
|
||||
|
||||
updateFade()
|
||||
|
||||
containerRef.addEventListener("scroll", updateFade, { passive: true })
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
lastScrollSize = 0
|
||||
lastClientSize = 0
|
||||
updateFade()
|
||||
})
|
||||
resizeObserver.observe(containerRef)
|
||||
|
||||
const mutationObserver = new MutationObserver(() => {
|
||||
lastScrollSize = 0
|
||||
lastClientSize = 0
|
||||
requestAnimationFrame(updateFade)
|
||||
})
|
||||
mutationObserver.observe(containerRef, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
})
|
||||
|
||||
let rafId: number
|
||||
const pollScroll = () => {
|
||||
updateFade()
|
||||
rafId = requestAnimationFrame(pollScroll)
|
||||
}
|
||||
rafId = requestAnimationFrame(pollScroll)
|
||||
|
||||
onCleanup(() => {
|
||||
containerRef?.removeEventListener("scroll", updateFade)
|
||||
resizeObserver.disconnect()
|
||||
mutationObserver.disconnect()
|
||||
cancelAnimationFrame(rafId)
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
local.children
|
||||
requestAnimationFrame(updateFade)
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(el) => {
|
||||
containerRef = el
|
||||
local.ref?.(el)
|
||||
}}
|
||||
data-component="scroll-fade"
|
||||
data-direction={direction()}
|
||||
data-scrollable={isScrollable() || undefined}
|
||||
data-fade-start={fadeStart() > 0 || undefined}
|
||||
data-fade-end={fadeEnd() > 0 || undefined}
|
||||
class={local.class}
|
||||
style={{
|
||||
...(typeof local.style === "object" ? local.style : {}),
|
||||
"--scroll-fade-start": `${fadeStart()}px`,
|
||||
"--scroll-fade-end": `${fadeEnd()}px`,
|
||||
}}
|
||||
{...others}
|
||||
>
|
||||
{local.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
import { type JSX, onCleanup, splitProps } from "solid-js"
|
||||
import { ScrollFade, type ScrollFadeProps } from "./scroll-fade"
|
||||
|
||||
const SCROLL_SPEED = 60
|
||||
const PAUSE_DURATION = 800
|
||||
|
||||
interface ScrollAnimationState {
|
||||
rafId: number | null
|
||||
startTime: number
|
||||
running: boolean
|
||||
}
|
||||
|
||||
const startScrollAnimation = (containerEl: HTMLElement): ScrollAnimationState | null => {
|
||||
containerEl.offsetHeight
|
||||
|
||||
const extraWidth = containerEl.scrollWidth - containerEl.clientWidth
|
||||
if (extraWidth <= 0) return null
|
||||
|
||||
const scrollDuration = (extraWidth / SCROLL_SPEED) * 1000
|
||||
const totalDuration = PAUSE_DURATION + scrollDuration + PAUSE_DURATION + scrollDuration + PAUSE_DURATION
|
||||
|
||||
const state: ScrollAnimationState = {
|
||||
rafId: null,
|
||||
startTime: performance.now(),
|
||||
running: true,
|
||||
}
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
if (!state.running) return
|
||||
|
||||
const elapsed = currentTime - state.startTime
|
||||
const progress = (elapsed % totalDuration) / totalDuration
|
||||
|
||||
const pausePercent = PAUSE_DURATION / totalDuration
|
||||
const scrollPercent = scrollDuration / totalDuration
|
||||
|
||||
const pauseEnd1 = pausePercent
|
||||
const scrollEnd1 = pauseEnd1 + scrollPercent
|
||||
const pauseEnd2 = scrollEnd1 + pausePercent
|
||||
const scrollEnd2 = pauseEnd2 + scrollPercent
|
||||
|
||||
let scrollPos = 0
|
||||
|
||||
if (progress < pauseEnd1) {
|
||||
scrollPos = 0
|
||||
} else if (progress < scrollEnd1) {
|
||||
const scrollProgress = (progress - pauseEnd1) / scrollPercent
|
||||
scrollPos = scrollProgress * extraWidth
|
||||
} else if (progress < pauseEnd2) {
|
||||
scrollPos = extraWidth
|
||||
} else if (progress < scrollEnd2) {
|
||||
const scrollProgress = (progress - pauseEnd2) / scrollPercent
|
||||
scrollPos = extraWidth * (1 - scrollProgress)
|
||||
} else {
|
||||
scrollPos = 0
|
||||
}
|
||||
|
||||
containerEl.scrollLeft = scrollPos
|
||||
state.rafId = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
state.rafId = requestAnimationFrame(animate)
|
||||
return state
|
||||
}
|
||||
|
||||
const stopScrollAnimation = (state: ScrollAnimationState | null, containerEl?: HTMLElement) => {
|
||||
if (state) {
|
||||
state.running = false
|
||||
if (state.rafId !== null) {
|
||||
cancelAnimationFrame(state.rafId)
|
||||
}
|
||||
}
|
||||
if (containerEl) {
|
||||
containerEl.scrollLeft = 0
|
||||
}
|
||||
}
|
||||
|
||||
export interface ScrollRevealProps extends Omit<ScrollFadeProps, "direction"> {
|
||||
/** Delay before scroll animation starts on hover (ms). Default: 300 */
|
||||
hoverDelay?: number
|
||||
}
|
||||
|
||||
export function ScrollReveal(props: ScrollRevealProps) {
|
||||
const [local, others] = splitProps(props, ["children", "hoverDelay", "ref"])
|
||||
|
||||
const hoverDelay = () => local.hoverDelay ?? 300
|
||||
|
||||
let containerRef: HTMLDivElement | undefined
|
||||
let hoverTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
let scrollAnimationState: ScrollAnimationState | null = null
|
||||
|
||||
const handleMouseEnter: JSX.EventHandler<HTMLDivElement, MouseEvent> = () => {
|
||||
hoverTimeout = setTimeout(() => {
|
||||
if (!containerRef) return
|
||||
|
||||
containerRef.offsetHeight
|
||||
|
||||
const isScrollable = containerRef.scrollWidth > containerRef.clientWidth + 1
|
||||
|
||||
if (isScrollable) {
|
||||
stopScrollAnimation(scrollAnimationState, containerRef)
|
||||
scrollAnimationState = startScrollAnimation(containerRef)
|
||||
}
|
||||
}, hoverDelay())
|
||||
}
|
||||
|
||||
const handleMouseLeave: JSX.EventHandler<HTMLDivElement, MouseEvent> = () => {
|
||||
if (hoverTimeout) {
|
||||
clearTimeout(hoverTimeout)
|
||||
hoverTimeout = undefined
|
||||
}
|
||||
stopScrollAnimation(scrollAnimationState, containerRef)
|
||||
scrollAnimationState = null
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (hoverTimeout) {
|
||||
clearTimeout(hoverTimeout)
|
||||
}
|
||||
stopScrollAnimation(scrollAnimationState, containerRef)
|
||||
})
|
||||
|
||||
return (
|
||||
<ScrollFade
|
||||
ref={(el) => {
|
||||
containerRef = el
|
||||
local.ref?.(el)
|
||||
}}
|
||||
fadeStartSize={8}
|
||||
fadeEndSize={8}
|
||||
direction="horizontal"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
{...others}
|
||||
>
|
||||
{local.children}
|
||||
</ScrollFade>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
[data-component="select"] {
|
||||
[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;
|
||||
transition: background-color 200ms cubic-bezier(0.25, 0, 0.5, 1);
|
||||
|
||||
[data-slot="select-select-trigger-value"] {
|
||||
overflow: hidden;
|
||||
|
|
@ -16,9 +20,10 @@
|
|||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-weak);
|
||||
transition: transform 0.1s ease-in-out;
|
||||
transition: transform 200ms cubic-bezier(0.25, 0, 0.5, 1);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&[data-expanded] {
|
||||
&[data-variant="secondary"] {
|
||||
background-color: var(--button-secondary-hover);
|
||||
|
|
@ -46,21 +51,26 @@
|
|||
}
|
||||
|
||||
[data-component="select-content"] {
|
||||
min-width: 4rem;
|
||||
min-width: 8rem;
|
||||
max-width: 23rem;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--surface-raised-stronger-non-alpha);
|
||||
padding: 2px;
|
||||
padding: 4px;
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
z-index: 50;
|
||||
transform-origin: var(--kb-popper-content-transform-origin);
|
||||
pointer-events: none;
|
||||
|
||||
&[data-closed] {
|
||||
animation: select-close 0.15s ease-out;
|
||||
animation: selectContentHide 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
|
||||
|
||||
@starting-style {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
animation: select-open 0.15s ease-out;
|
||||
pointer-events: auto;
|
||||
animation: selectContentShow 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
|
||||
}
|
||||
|
||||
[data-slot="select-select-content-list"] {
|
||||
|
|
@ -87,13 +97,13 @@
|
|||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 6px 0 6px;
|
||||
padding: 4px 8px;
|
||||
gap: 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
|
|
@ -102,13 +112,13 @@
|
|||
color: var(--text-strong);
|
||||
|
||||
transition:
|
||||
background-color 0.2s ease-in-out,
|
||||
color 0.2s ease-in-out;
|
||||
background-color 200ms cubic-bezier(0.25, 0, 0.5, 1),
|
||||
color 200ms cubic-bezier(0.25, 0, 0.5, 1);
|
||||
outline: none;
|
||||
user-select: none;
|
||||
|
||||
&[data-highlighted] {
|
||||
background: var(--surface-raised-base-hover);
|
||||
&:hover {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
&[data-disabled] {
|
||||
background-color: var(--surface-raised-base);
|
||||
|
|
@ -131,24 +141,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes select-open {
|
||||
@keyframes selectContentShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
transform: scaleY(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes select-close {
|
||||
@keyframes selectContentHide {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transform: scaleY(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
transform: scaleY(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
|
|||
placement="bottom-start"
|
||||
value={local.current}
|
||||
options={grouped()}
|
||||
gutter={12}
|
||||
optionValue={(x) => (local.value ? local.value(x) : (x as string))}
|
||||
optionTextValue={(x) => (local.label ? local.label(x) : (x as string))}
|
||||
optionGroupChildren="options"
|
||||
|
|
@ -73,7 +74,7 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
|
|||
: (itemProps.item.rawValue as string)}
|
||||
</Kobalte.ItemLabel>
|
||||
<Kobalte.ItemIndicator data-slot="select-select-item-indicator">
|
||||
<Icon name="check-small" size="small" />
|
||||
<Icon name="check" size="small" class="text-icon-strong-base" />
|
||||
</Kobalte.ItemIndicator>
|
||||
</Kobalte.Item>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -72,17 +72,17 @@ export const SessionReview = (props: SessionReviewProps) => {
|
|||
<div data-slot="session-review-actions">
|
||||
<Show when={props.onDiffStyleChange}>
|
||||
<RadioGroup
|
||||
options={["unified", "split"] as const}
|
||||
options={["unified", "split"]}
|
||||
current={diffStyle()}
|
||||
value={(style) => style}
|
||||
label={(style) => (style === "unified" ? "Unified" : "Split")}
|
||||
onSelect={(style) => style && props.onDiffStyleChange?.(style)}
|
||||
onSelect={(style) => style && props.onDiffStyleChange?.(style as SessionReviewDiffStyle)}
|
||||
/>
|
||||
</Show>
|
||||
<Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}>
|
||||
<Switch>
|
||||
<Match when={open().length > 0}>Collapse all</Match>
|
||||
<Match when={true}>Expand all</Match>
|
||||
<Match when={open().length > 0}>Collapse</Match>
|
||||
<Match when={true}>Expand</Match>
|
||||
</Switch>
|
||||
</Button>
|
||||
{props.actions}
|
||||
|
|
|
|||
Loading…
Reference in New Issue