summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-31 07:18:51 -0600
committerGitHub <[email protected]>2026-01-31 07:18:51 -0600
commita552652fcc024752fcd43d9117282307d15c44d1 (patch)
tree67631d8dd33f1b4db65ea6d2518c188faf1f066f
parent511c7abacaebb5e007139395078b6a848e5e9ea5 (diff)
downloadopencode-a552652fcc024752fcd43d9117282307d15c44d1.tar.gz
opencode-a552652fcc024752fcd43d9117282307d15c44d1.zip
Revert "feat: Transitions, spacing, scroll fade, prompt area update (#11168)" (#11461)
Co-authored-by: adamelmore <[email protected]>
-rw-r--r--packages/app/src/components/dialog-select-model.tsx7
-rw-r--r--packages/app/src/components/prompt-input.tsx124
-rw-r--r--packages/app/src/components/session-context-usage.tsx6
-rw-r--r--packages/app/src/components/settings-general.tsx10
-rw-r--r--packages/app/src/components/settings-keybinds.tsx10
-rw-r--r--packages/desktop/src-tauri/src/lib.rs1
-rw-r--r--packages/ui/src/components/accordion.css69
-rw-r--r--packages/ui/src/components/accordion.tsx43
-rw-r--r--packages/ui/src/components/button.css29
-rw-r--r--packages/ui/src/components/button.tsx2
-rw-r--r--packages/ui/src/components/card.css4
-rw-r--r--packages/ui/src/components/checkbox.css12
-rw-r--r--packages/ui/src/components/collapsible.css44
-rw-r--r--packages/ui/src/components/collapsible.tsx42
-rw-r--r--packages/ui/src/components/cycle-label.css51
-rw-r--r--packages/ui/src/components/cycle-label.tsx132
-rw-r--r--packages/ui/src/components/dialog.css33
-rw-r--r--packages/ui/src/components/dropdown-menu.css45
-rw-r--r--packages/ui/src/components/hover-card.css4
-rw-r--r--packages/ui/src/components/icon-button.css5
-rw-r--r--packages/ui/src/components/icon.css2
-rw-r--r--packages/ui/src/components/icon.tsx7
-rw-r--r--packages/ui/src/components/list.css32
-rw-r--r--packages/ui/src/components/list.tsx5
-rw-r--r--packages/ui/src/components/logo.css2
-rw-r--r--packages/ui/src/components/message-part.tsx3
-rw-r--r--packages/ui/src/components/morph-chevron.css10
-rw-r--r--packages/ui/src/components/morph-chevron.tsx73
-rw-r--r--packages/ui/src/components/popover.css58
-rw-r--r--packages/ui/src/components/progress-circle.css10
-rw-r--r--packages/ui/src/components/progress-circle.tsx46
-rw-r--r--packages/ui/src/components/radio-group.css17
-rw-r--r--packages/ui/src/components/reasoning-icon.css8
-rw-r--r--packages/ui/src/components/reasoning-icon.tsx46
-rw-r--r--packages/ui/src/components/resize-handle.css4
-rw-r--r--packages/ui/src/components/scroll-fade.css82
-rw-r--r--packages/ui/src/components/scroll-fade.tsx207
-rw-r--r--packages/ui/src/components/scroll-reveal.tsx141
-rw-r--r--packages/ui/src/components/select.css135
-rw-r--r--packages/ui/src/components/select.tsx18
-rw-r--r--packages/ui/src/components/session-review.css12
-rw-r--r--packages/ui/src/components/session-review.tsx5
-rw-r--r--packages/ui/src/components/session-turn.css15
-rw-r--r--packages/ui/src/components/switch.css12
-rw-r--r--packages/ui/src/components/tabs.css3
-rw-r--r--packages/ui/src/components/tag.css3
-rw-r--r--packages/ui/src/context/dialog.tsx28
-rw-r--r--packages/ui/src/styles/index.css2
-rw-r--r--packages/ui/src/styles/utilities.css42
49 files changed, 380 insertions, 1321 deletions
diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx
index 4d96c6c5f..4f0dcc3ee 100644
--- a/packages/app/src/components/dialog-select-model.tsx
+++ b/packages/app/src/components/dialog-select-model.tsx
@@ -90,7 +90,7 @@ const ModelList: Component<{
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
provider?: string
- children?: JSX.Element | ((open: boolean) => JSX.Element)
+ children?: JSX.Element
triggerAs?: T
triggerProps?: ComponentProps<T>
}) {
@@ -182,13 +182,12 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
as={props.triggerAs ?? "div"}
{...(props.triggerProps as any)}
>
- {typeof props.children === "function" ? props.children(store.open) : props.children}
+ {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"
- data-component="model-popover-content"
ref={(el) => setStore("content", el)}
+ class="w-72 h-80 flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
onEscapeKeyDown={(event) => {
setStore("dismiss", "escape")
setStore("open", false)
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index 6e614eb1d..5c1d417eb 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -32,9 +32,7 @@ import { useNavigate, useParams } from "@solidjs/router"
import { useSync } from "@/context/sync"
import { useComments } from "@/context/comments"
import { FileIcon } from "@opencode-ai/ui/file-icon"
-import { MorphChevron } from "@opencode-ai/ui/morph-chevron"
import { Button } from "@opencode-ai/ui/button"
-import { CycleLabel } from "@opencode-ai/ui/cycle-label"
import { Icon } from "@opencode-ai/ui/icon"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import type { IconName } from "@opencode-ai/ui/icons/provider"
@@ -44,7 +42,6 @@ import { Select } from "@opencode-ai/ui/select"
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/util/path"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ImagePreview } from "@opencode-ai/ui/image-preview"
-import { ReasoningIcon } from "@opencode-ai/ui/reasoning-icon"
import { ModelSelectorPopover } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useProviders } from "@/hooks/use-providers"
@@ -1255,7 +1252,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
clearInput()
client.session
.shell({
- sessionID: session?.id || "",
+ sessionID: session.id,
agent,
model,
command: text,
@@ -1278,7 +1275,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
clearInput()
client.session
.command({
- sessionID: session?.id || "",
+ sessionID: session.id,
command: commandName,
arguments: args.join(" "),
agent,
@@ -1434,13 +1431,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const optimisticParts = requestParts.map((part) => ({
...part,
- sessionID: session?.id || "",
+ sessionID: session.id,
messageID,
})) as unknown as Part[]
const optimisticMessage: Message = {
id: messageID,
- sessionID: session?.id || "",
+ sessionID: session.id,
role: "user",
time: { created: Date.now() },
agent,
@@ -1451,9 +1448,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (sessionDirectory === projectDirectory) {
sync.set(
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)
@@ -1469,9 +1466,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
globalSync.child(sessionDirectory)[1](
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)
@@ -1488,7 +1485,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (sessionDirectory === projectDirectory) {
sync.set(
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)
@@ -1501,7 +1498,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
globalSync.child(sessionDirectory)[1](
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)
@@ -1522,15 +1519,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const worktree = WorktreeState.get(sessionDirectory)
if (!worktree || worktree.status !== "pending") return true
- if (sessionDirectory === projectDirectory && session?.id) {
- sync.set("session_status", session?.id, { type: "busy" })
+ if (sessionDirectory === projectDirectory) {
+ sync.set("session_status", session.id, { type: "busy" })
}
const controller = new AbortController()
const cleanup = () => {
- if (sessionDirectory === projectDirectory && session?.id) {
- sync.set("session_status", session?.id, { type: "idle" })
+ if (sessionDirectory === projectDirectory) {
+ sync.set("session_status", session.id, { type: "idle" })
}
removeOptimisticMessage()
for (const item of commentItems) {
@@ -1547,7 +1544,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
restoreInput()
}
- pending.set(session?.id || "", { abort: controller, cleanup })
+ pending.set(session.id, { abort: controller, cleanup })
const abort = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
if (controller.signal.aborted) {
@@ -1575,7 +1572,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (timer.id === undefined) return
clearTimeout(timer.id)
})
- pending.delete(session?.id || "")
+ pending.delete(session.id)
if (controller.signal.aborted) return false
if (result.status === "failed") throw new Error(result.message)
return true
@@ -1585,7 +1582,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const ok = await waitForWorktree()
if (!ok) return
await client.session.prompt({
- sessionID: session?.id || "",
+ sessionID: session.id,
agent,
model,
messageID,
@@ -1595,9 +1592,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
void send().catch((err) => {
- pending.delete(session?.id || "")
- if (sessionDirectory === projectDirectory && session?.id) {
- sync.set("session_status", session?.id, { type: "idle" })
+ pending.delete(session.id)
+ if (sessionDirectory === projectDirectory) {
+ sync.set("session_status", session.id, { type: "idle" })
}
showToast({
title: language.t("prompt.toast.promptSendFailed.title"),
@@ -1619,28 +1616,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
}
- const currrentModelVariant = createMemo(() => {
- const modelVariant = local.model.variant.current() ?? ""
- return modelVariant === "xhigh"
- ? "xHigh"
- : modelVariant.length > 0
- ? modelVariant[0].toUpperCase() + modelVariant.slice(1)
- : "Default"
- })
-
- const reasoningPercentage = createMemo(() => {
- const variants = local.model.variant.list()
- const current = local.model.variant.current()
- const totalEntries = variants.length + 1
-
- if (totalEntries <= 2 || current === "Default") {
- return 0
- }
-
- const currentIndex = current ? variants.indexOf(current) + 1 : 0
- return ((currentIndex + 1) / totalEntries) * 100
- }, [local.model.variant])
-
return (
<div class="relative size-full _max-h-[320px] flex flex-col gap-3">
<Show when={store.popover}>
@@ -1693,7 +1668,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</>
}
>
- <Icon name="brain" size="normal" class="text-icon-info-active shrink-0" />
+ <Icon name="brain" size="small" class="text-icon-info-active shrink-0" />
<span class="text-14-regular text-text-strong whitespace-nowrap">
@{(item as { type: "agent"; name: string }).name}
</span>
@@ -1754,9 +1729,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}}
>
<Show when={store.dragging}>
- <div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-raised-stronger-non-alpha/90 mr-1 pointer-events-none">
+ <div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-raised-stronger-non-alpha/90 pointer-events-none">
<div class="flex flex-col items-center gap-2 text-text-weak">
- <Icon name="photo" size={18} class="text-icon-base stroke-1.5" />
+ <Icon name="photo" class="size-8" />
<span class="text-14-regular">{language.t("prompt.dropzone.label")}</span>
</div>
</div>
@@ -1795,7 +1770,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}}
>
<div class="flex items-center gap-1.5">
- <FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-7" />
+ <FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
<div class="flex items-center text-11-regular min-w-0 font-medium">
<span class="text-text-strong whitespace-nowrap">{getFilenameTruncated(item.path, 14)}</span>
<Show when={item.selection}>
@@ -1812,7 +1787,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
type="button"
icon="close-small"
variant="ghost"
- class="ml-auto size-7 opacity-0 group-hover:opacity-100 transition-all"
+ class="ml-auto h-5 w-5 opacity-0 group-hover:opacity-100 transition-all"
onClick={(e) => {
e.stopPropagation()
if (item.commentID) comments.remove(item.path, item.commentID)
@@ -1842,7 +1817,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
when={attachment.mime.startsWith("image/")}
fallback={
<div class="size-16 rounded-md bg-surface-base flex items-center justify-center border border-border-base">
- <Icon name="folder" size="normal" class="size-6 text-text-base" />
+ <Icon name="folder" class="size-6 text-text-weak" />
</div>
}
>
@@ -1916,7 +1891,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-2">
+ <div class="flex items-center justify-start gap-0.5">
<Switch>
<Match when={store.mode === "shell"}>
<div class="flex items-center gap-2 px-2 h-6">
@@ -1947,17 +1922,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
- <Button
- as="div"
- variant="ghost"
- class="px-2"
- onClick={() => dialog.render(<DialogSelectModelUnpaid />, "select-model")}
- >
+ <Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
</Show>
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
- <MorphChevron expanded={dialog.isActive("select-model")} />
+ <Icon name="chevron-down" size="small" />
</Button>
</TooltipKeybind>
}
@@ -1968,15 +1938,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
keybind={command.keybind("model.choose")}
>
<ModelSelectorPopover triggerAs={Button} triggerProps={{ variant: "ghost" }}>
- {(open) => (
- <>
- <Show when={local.model.current()?.provider?.id}>
- <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
- </Show>
- {local.model.current()?.name ?? language.t("dialog.model.select.title")}
- <MorphChevron expanded={open} class="text-text-weak" />
- </>
- )}
+ <Show when={local.model.current()?.provider?.id}>
+ <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
+ </Show>
+ {local.model.current()?.name ?? language.t("dialog.model.select.title")}
+ <Icon name="chevron-down" size="small" />
</ModelSelectorPopover>
</TooltipKeybind>
</Show>
@@ -1989,13 +1955,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Button
data-action="model-variant-cycle"
variant="ghost"
- class="text-text-strong text-12-regular"
+ class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
onClick={() => local.model.variant.cycle()}
>
- <Show when={local.model.variant.list().length > 1}>
- <ReasoningIcon percentage={reasoningPercentage()} size={16} strokeWidth={1.25} />
- </Show>
- <CycleLabel value={currrentModelVariant()} />
+ {local.model.variant.current() ?? language.t("common.default")}
</Button>
</TooltipKeybind>
</Show>
@@ -2009,7 +1972,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
variant="ghost"
onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
classList={{
- "_hidden group-hover/prompt-input:flex items-center justify-center": true,
+ "_hidden group-hover/prompt-input:flex size-6 items-center justify-center": true,
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
}}
@@ -2031,7 +1994,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Match>
</Switch>
</div>
- <div class="flex items-center gap-1 absolute right-3 bottom-3">
+ <div class="flex items-center gap-3 absolute right-3 bottom-3">
<input
ref={fileInputRef}
type="file"
@@ -2043,19 +2006,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
e.currentTarget.value = ""
}}
/>
- <div class="flex items-center gap-1.5 mr-1.5">
+ <div class="flex items-center gap-2">
<SessionContextUsage />
<Show when={store.mode === "normal"}>
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
<Button
type="button"
variant="ghost"
- size="small"
- class="px-1"
+ class="size-6"
onClick={() => fileInputRef.click()}
aria-label={language.t("prompt.action.attachFile")}
>
- <Icon name="photo" class="size-6 text-icon-base" />
+ <Icon name="photo" class="size-4.5" />
</Button>
</Tooltip>
</Show>
@@ -2074,7 +2036,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Match when={true}>
<div class="flex items-center gap-2">
<span>{language.t("prompt.action.send")}</span>
- <Icon name="enter" size="normal" class="text-icon-base" />
+ <Icon name="enter" size="small" class="text-icon-base" />
</div>
</Match>
</Switch>
@@ -2085,7 +2047,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
disabled={!prompt.dirty() && !working()}
icon={working() ? "stop" : "arrow-up"}
variant="primary"
- class="h-6 w-5.5"
+ class="h-6 w-4.5"
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
/>
</Tooltip>
diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx
index 92b060212..1e37d8f6a 100644
--- a/packages/app/src/components/session-context-usage.tsx
+++ b/packages/app/src/components/session-context-usage.tsx
@@ -64,8 +64,8 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
}
const circle = () => (
- <div class="text-icon-base">
- <ProgressCircle size={18} percentage={context()?.percentage ?? 0} />
+ <div class="p-1">
+ <ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
</div>
)
@@ -101,7 +101,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
<Button
type="button"
variant="ghost"
- class="size-7 text-icon-base"
+ class="size-6"
onClick={openContext}
aria-label={language.t("context.usage.view")}
>
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
index b2fb43536..b26f6ba22 100644
--- a/packages/app/src/components/settings-general.tsx
+++ b/packages/app/src/components/settings-general.tsx
@@ -10,7 +10,6 @@ import { usePlatform } from "@/context/platform"
import { useSettings, monoFontFamily } from "@/context/settings"
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "./link"
-import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
let demoSoundState = {
cleanup: undefined as (() => void) | undefined,
@@ -131,12 +130,7 @@ export const SettingsGeneral: Component = () => {
const soundOptions = [...SOUND_OPTIONS]
return (
- <ScrollFade
- direction="vertical"
- fadeStartSize={0}
- fadeEndSize={16}
- class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"
- >
+ <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-1 pt-6 pb-8">
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
@@ -401,7 +395,7 @@ export const SettingsGeneral: Component = () => {
</div>
</div>
</div>
- </ScrollFade>
+ </div>
)
}
diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx
index efd18c8bd..393da0c2a 100644
--- a/packages/app/src/components/settings-keybinds.tsx
+++ b/packages/app/src/components/settings-keybinds.tsx
@@ -9,7 +9,6 @@ import fuzzysort from "fuzzysort"
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
-import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
const PALETTE_ID = "command.palette"
@@ -353,12 +352,7 @@ export const SettingsKeybinds: Component = () => {
})
return (
- <ScrollFade
- direction="vertical"
- fadeStartSize={0}
- fadeEndSize={16}
- class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"
- >
+ <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
<div class="flex items-center justify-between gap-4">
@@ -435,6 +429,6 @@ export const SettingsKeybinds: Component = () => {
</div>
</Show>
</div>
- </ScrollFade>
+ </div>
)
}
diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs
index d16416c9f..29ac86f29 100644
--- a/packages/desktop/src-tauri/src/lib.rs
+++ b/packages/desktop/src-tauri/src/lib.rs
@@ -345,7 +345,6 @@ pub fn run() {
.decorations(false);
let window = window_builder.build().expect("Failed to create window");
- let _ = window.show();
#[cfg(windows)]
let _ = window.create_overlay_titlebar();
diff --git a/packages/ui/src/components/accordion.css b/packages/ui/src/components/accordion.css
index b310eeedb..7bf287fe5 100644
--- a/packages/ui/src/components/accordion.css
+++ b/packages/ui/src/components/accordion.css
@@ -36,9 +36,7 @@
border-radius: var(--radius-md);
overflow: clip;
color: var(--text-strong);
- transition-property: background-color, border-color;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition: background-color 0.15s ease;
/* text-12-regular */
font-family: var(--font-family-sans);
@@ -60,48 +58,41 @@
}
}
- [data-slot="accordion-arrow"] {
- flex-shrink: 0;
- width: 16px;
- height: 16px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--text-weak);
- }
-
- [data-slot="accordion-content"] {
- display: grid;
- grid-template-rows: 0fr;
- transition-property: grid-template-rows, opacity;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
- width: 100%;
-
- > * {
- overflow: hidden;
+ &[data-expanded] {
+ [data-slot="accordion-trigger"] {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
}
- }
- [data-slot="accordion-content"][data-expanded] {
- grid-template-rows: 1fr;
+ [data-slot="accordion-content"] {
+ border: 1px solid var(--border-weak-base);
+ border-top: none;
+ border-bottom-left-radius: var(--radius-md);
+ border-bottom-right-radius: var(--radius-md);
+ }
}
- [data-slot="accordion-content"][data-closed] {
- grid-template-rows: 0fr;
+ [data-slot="accordion-content"] {
+ overflow: hidden;
+ width: 100%;
}
+ }
+}
- &[data-expanded] [data-slot="accordion-trigger"] {
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- }
+@keyframes slideDown {
+ from {
+ height: 0;
+ }
+ to {
+ height: var(--kb-accordion-content-height);
+ }
+}
- &[data-expanded] [data-slot="accordion-content"] {
- border: 1px solid var(--border-weak-base);
- border-top: none;
- border-bottom-left-radius: var(--radius-md);
- border-bottom-right-radius: var(--radius-md);
- height: auto;
- }
+@keyframes slideUp {
+ from {
+ height: var(--kb-accordion-content-height);
+ }
+ to {
+ height: 0;
}
}
diff --git a/packages/ui/src/components/accordion.tsx b/packages/ui/src/components/accordion.tsx
index e30be95e0..535d38e3d 100644
--- a/packages/ui/src/components/accordion.tsx
+++ b/packages/ui/src/components/accordion.tsx
@@ -1,7 +1,6 @@
import { Accordion as Kobalte } from "@kobalte/core/accordion"
-import { Accessor, createContext, splitProps, useContext } from "solid-js"
+import { splitProps } from "solid-js"
import type { ComponentProps, ParentProps } from "solid-js"
-import { MorphChevron } from "./morph-chevron"
export interface AccordionProps extends ComponentProps<typeof Kobalte> {}
export interface AccordionItemProps extends ComponentProps<typeof Kobalte.Item> {}
@@ -9,8 +8,6 @@ export interface AccordionHeaderProps extends ComponentProps<typeof Kobalte.Head
export interface AccordionTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {}
export interface AccordionContentProps extends ComponentProps<typeof Kobalte.Content> {}
-const AccordionItemContext = createContext<Accessor<boolean>>()
-
function AccordionRoot(props: AccordionProps) {
const [split, rest] = splitProps(props, ["class", "classList"])
return (
@@ -25,19 +22,17 @@ function AccordionRoot(props: AccordionProps) {
)
}
-function AccordionItem(props: AccordionItemProps & { expanded?: boolean }) {
- const [split, rest] = splitProps(props, ["class", "classList", "expanded"])
+function AccordionItem(props: AccordionItemProps) {
+ const [split, rest] = splitProps(props, ["class", "classList"])
return (
- <AccordionItemContext.Provider value={() => split.expanded ?? false}>
- <Kobalte.Item
- {...rest}
- data-slot="accordion-item"
- classList={{
- ...(split.classList ?? {}),
- [split.class ?? ""]: !!split.class,
- }}
- />
- </AccordionItemContext.Provider>
+ <Kobalte.Item
+ {...rest}
+ data-slot="accordion-item"
+ classList={{
+ ...(split.classList ?? {}),
+ [split.class ?? ""]: !!split.class,
+ }}
+ />
)
}
@@ -89,25 +84,9 @@ function AccordionContent(props: ParentProps<AccordionContentProps>) {
)
}
-export interface AccordionArrowProps extends ComponentProps<"div"> {
- expanded?: boolean
-}
-
-function AccordionArrow(props: AccordionArrowProps = {}) {
- const [local, rest] = splitProps(props, ["expanded"])
- const contextExpanded = useContext(AccordionItemContext)
- const isExpanded = () => local.expanded ?? contextExpanded?.() ?? false
- return (
- <div data-slot="accordion-arrow" {...rest}>
- <MorphChevron expanded={isExpanded()} />
- </div>
- )
-}
-
export const Accordion = Object.assign(AccordionRoot, {
Item: AccordionItem,
Header: AccordionHeader,
Trigger: AccordionTrigger,
Content: AccordionContent,
- Arrow: AccordionArrow,
})
diff --git a/packages/ui/src/components/button.css b/packages/ui/src/components/button.css
index 02a7ade71..d9b345923 100644
--- a/packages/ui/src/components/button.css
+++ b/packages/ui/src/components/button.css
@@ -8,13 +8,8 @@
text-decoration: none;
user-select: none;
cursor: default;
- padding: 4px 8px;
- white-space: nowrap;
- transition-property: background-color, border-color, color, box-shadow, opacity;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
outline: none;
- line-height: 20px;
+ white-space: nowrap;
&[data-variant="primary"] {
background-color: var(--button-primary-base);
@@ -99,6 +94,7 @@
&:active:not(:disabled) {
background-color: var(--button-secondary-base);
scale: 0.99;
+ transition: all 150ms ease-out;
}
&:disabled {
border-color: var(--border-disabled);
@@ -113,27 +109,34 @@
}
&[data-size="small"] {
- padding: 2px 8px;
+ height: 22px;
+ padding: 0 8px;
&[data-icon] {
- padding: 2px 12px 2px 4px;
+ 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-base);
+ font-size: var(--font-size-small);
font-style: normal;
font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large); /* 166.667% */
letter-spacing: var(--letter-spacing-normal);
}
&[data-size="normal"] {
- padding: 4px 6px;
+ height: 24px;
+ line-height: 24px;
+ padding: 0 6px;
&[data-icon] {
- padding: 4px 12px 4px 4px;
+ padding: 0 12px 0 4px;
}
+ font-size: var(--font-size-small);
gap: 6px;
/* text-12-medium */
@@ -145,10 +148,11 @@
}
&[data-size="large"] {
+ height: 32px;
padding: 6px 12px;
&[data-icon] {
- padding: 6px 12px 6px 8px;
+ padding: 0 12px 0 8px;
}
gap: 4px;
@@ -158,6 +162,7 @@
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large); /* 142.857% */
letter-spacing: var(--letter-spacing-normal);
}
diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx
index b2d2004d3..7f974b2f7 100644
--- a/packages/ui/src/components/button.tsx
+++ b/packages/ui/src/components/button.tsx
@@ -4,7 +4,7 @@ import { Icon, IconProps } from "./icon"
export interface ButtonProps
extends ComponentProps<typeof Kobalte>,
- Pick<ComponentProps<"button">, "class" | "classList" | "children" | "style"> {
+ Pick<ComponentProps<"button">, "class" | "classList" | "children"> {
size?: "small" | "normal" | "large"
variant?: "primary" | "secondary" | "ghost"
icon?: IconProps["name"]
diff --git a/packages/ui/src/components/card.css b/packages/ui/src/components/card.css
index 809fbdacd..6dae47223 100644
--- a/packages/ui/src/components/card.css
+++ b/packages/ui/src/components/card.css
@@ -4,9 +4,7 @@
flex-direction: column;
background-color: var(--surface-inset-base);
border: 1px solid var(--border-weaker-base);
- transition-property: background-color, border-color;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition: background-color 0.15s ease;
border-radius: var(--radius-md);
padding: 6px 12px;
overflow: clip;
diff --git a/packages/ui/src/components/checkbox.css b/packages/ui/src/components/checkbox.css
index cad0dd2dd..b10ebbbd1 100644
--- a/packages/ui/src/components/checkbox.css
+++ b/packages/ui/src/components/checkbox.css
@@ -4,18 +4,6 @@
gap: 12px;
cursor: default;
- [data-slot="checkbox-checkbox-control"] {
- transition-property: border-color, background-color, box-shadow;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
- }
-
- [data-slot="checkbox-checkbox-indicator"] {
- transition-property: opacity;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
- }
-
[data-slot="checkbox-checkbox-input"] {
position: absolute;
width: 1px;
diff --git a/packages/ui/src/components/collapsible.css b/packages/ui/src/components/collapsible.css
index cc62b2b87..1f20cf85d 100644
--- a/packages/ui/src/components/collapsible.css
+++ b/packages/ui/src/components/collapsible.css
@@ -4,9 +4,7 @@
flex-direction: column;
background-color: var(--surface-inset-base);
border: 1px solid var(--border-weaker-base);
- transition-property: background-color, border-color;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition: background-color 0.15s ease;
border-radius: var(--radius-md);
overflow: clip;
@@ -46,28 +44,16 @@
display: flex;
align-items: center;
justify-content: center;
- color: var(--text-weak);
}
}
[data-slot="collapsible-content"] {
- display: grid;
- grid-template-rows: 0fr;
- transition-property: grid-template-rows, opacity;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ overflow: hidden;
+ /* animation: slideUp 250ms ease-out; */
- > * {
- overflow: hidden;
- }
-
- &[data-expanded] {
- grid-template-rows: 1fr;
- }
-
- &[data-closed] {
- grid-template-rows: 0fr;
- }
+ /* &[data-expanded] { */
+ /* animation: slideDown 250ms ease-out; */
+ /* } */
}
&[data-variant="ghost"] {
@@ -97,3 +83,21 @@
}
}
}
+
+@keyframes slideDown {
+ from {
+ height: 0;
+ }
+ to {
+ height: var(--kb-collapsible-content-height);
+ }
+}
+
+@keyframes slideUp {
+ from {
+ height: var(--kb-collapsible-content-height);
+ }
+ to {
+ height: 0;
+ }
+}
diff --git a/packages/ui/src/components/collapsible.tsx b/packages/ui/src/components/collapsible.tsx
index 55b7b6033..903afc308 100644
--- a/packages/ui/src/components/collapsible.tsx
+++ b/packages/ui/src/components/collapsible.tsx
@@ -1,8 +1,6 @@
import { Collapsible as Kobalte, CollapsibleRootProps } from "@kobalte/core/collapsible"
-import { Accessor, ComponentProps, createContext, createSignal, ParentProps, splitProps, useContext } from "solid-js"
-import { MorphChevron } from "./morph-chevron"
-
-const CollapsibleContext = createContext<Accessor<boolean>>()
+import { ComponentProps, ParentProps, splitProps } from "solid-js"
+import { Icon } from "./icon"
export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> {
class?: string
@@ -11,30 +9,17 @@ export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> {
}
function CollapsibleRoot(props: CollapsibleProps) {
- const [local, others] = splitProps(props, ["class", "classList", "variant", "open", "onOpenChange", "children"])
- const [internalOpen, setInternalOpen] = createSignal(local.open ?? false)
-
- const handleOpenChange = (open: boolean) => {
- setInternalOpen(open)
- local.onOpenChange?.(open)
- }
-
+ const [local, others] = splitProps(props, ["class", "classList", "variant"])
return (
- <CollapsibleContext.Provider value={internalOpen}>
- <Kobalte
- data-component="collapsible"
- data-variant={local.variant || "normal"}
- open={local.open}
- onOpenChange={handleOpenChange}
- classList={{
- ...(local.classList ?? {}),
- [local.class ?? ""]: !!local.class,
- }}
- {...others}
- >
- {local.children}
- </Kobalte>
- </CollapsibleContext.Provider>
+ <Kobalte
+ data-component="collapsible"
+ data-variant={local.variant || "normal"}
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ {...others}
+ />
)
}
@@ -47,10 +32,9 @@ function CollapsibleContent(props: ComponentProps<typeof Kobalte.Content>) {
}
function CollapsibleArrow(props?: ComponentProps<"div">) {
- const isOpen = useContext(CollapsibleContext)
return (
<div data-slot="collapsible-arrow" {...(props || {})}>
- <MorphChevron expanded={isOpen?.() ?? false} />
+ <Icon name="chevron-grabber-vertical" size="small" />
</div>
)
}
diff --git a/packages/ui/src/components/cycle-label.css b/packages/ui/src/components/cycle-label.css
deleted file mode 100644
index e3b5256d4..000000000
--- a/packages/ui/src/components/cycle-label.css
+++ /dev/null
@@ -1,51 +0,0 @@
-.cycle-label {
- --c-dur: 200ms;
- --c-stag: 30ms;
- --c-ease: cubic-bezier(0.25, 0, 0.5, 1);
- --c-opacity-start: 0;
- --c-opacity-end: 1;
- --c-blur-start: 0px;
- --c-blur-end: 0px;
- --c-skew: 10deg;
-
- display: inline-flex;
- position: relative;
-
- transform-style: preserve-3d;
- perspective: 500px;
- transition: width 200ms var(--c-ease);
- will-change: width;
- overflow: hidden;
-
- .cycle-char {
- display: inline-block;
- transform-style: preserve-3d;
- min-width: 0.25em;
- backface-visibility: hidden;
-
- transition:
- transform var(--c-dur) var(--c-ease),
- opacity var(--c-dur) var(--c-ease),
- filter var(--c-dur) var(--c-ease);
- transition-delay: calc(var(--i, 0) * var(--c-stag));
-
- &.enter {
- opacity: var(--c-opacity-end);
- filter: blur(var(--c-blur-end));
- transform: translateY(0) rotateX(0) skewX(0);
- }
-
- &.exit {
- opacity: var(--c-opacity-start);
- filter: blur(var(--c-blur-start));
- transform: translateY(50%) rotateX(90deg) skewX(var(--c-skew));
- }
-
- &.pre {
- opacity: var(--c-opacity-start);
- filter: blur(var(--c-blur-start));
- transition: none;
- transform: translateY(-50%) rotateX(-90deg) skewX(calc(var(--c-skew) * -1));
- }
- }
-}
diff --git a/packages/ui/src/components/cycle-label.tsx b/packages/ui/src/components/cycle-label.tsx
deleted file mode 100644
index e34385a2c..000000000
--- a/packages/ui/src/components/cycle-label.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import "./cycle-label.css"
-import { createEffect, createSignal, JSX, on } from "solid-js"
-
-export interface CycleLabelProps extends JSX.HTMLAttributes<HTMLSpanElement> {
- value: string
- onValueChange?: (value: string) => void
- duration?: number | ((value: string) => number)
- stagger?: number
- opacity?: [number, number]
- blur?: [number, number]
- skewX?: number
- onAnimationStart?: () => void
- onAnimationEnd?: () => void
-}
-
-const segmenter =
- typeof Intl !== "undefined" && Intl.Segmenter ? new Intl.Segmenter("en", { granularity: "grapheme" }) : null
-
-const getChars = (text: string): string[] =>
- segmenter ? Array.from(segmenter.segment(text), (s) => s.segment) : text.split("")
-
-const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
-
-export function CycleLabel(props: CycleLabelProps) {
- const getDuration = (text: string) => {
- const d = props?.duration ?? 200
- return typeof d === "function" ? d(text) : d
- }
- const stagger = () => props?.stagger ?? 20
- const opacity = () => props?.opacity ?? [0, 1]
- const blur = () => props?.blur ?? [0, 0]
- const skewX = () => props?.skewX ?? 10
-
- let containerRef: HTMLSpanElement | undefined
- let isAnimating = false
- const [currentText, setCurrentText] = createSignal(props.value)
-
- const setChars = (el: HTMLElement, text: string, state: "enter" | "exit" | "pre" = "enter") => {
- el.innerHTML = ""
- const chars = getChars(text)
- chars.forEach((char, i) => {
- const span = document.createElement("span")
- span.textContent = char === " " ? "\u00A0" : char
- span.className = `cycle-char ${state}`
- span.style.setProperty("--i", String(i))
- el.appendChild(span)
- })
- }
-
- const animateToText = async (newText: string) => {
- if (!containerRef || isAnimating) return
- if (newText === currentText()) return
-
- isAnimating = true
- props.onAnimationStart?.()
-
- const dur = getDuration(newText)
- const stag = stagger()
-
- containerRef.style.width = containerRef.offsetWidth + "px"
-
- const oldChars = containerRef.querySelectorAll(".cycle-char")
- oldChars.forEach((c) => c.classList.replace("enter", "exit"))
-
- const clone = containerRef.cloneNode(false) as HTMLElement
- Object.assign(clone.style, {
- position: "absolute",
- visibility: "hidden",
- width: "auto",
- transition: "none",
- })
- setChars(clone, newText)
- document.body.appendChild(clone)
- const nextWidth = clone.offsetWidth
- clone.remove()
-
- const exitTime = oldChars.length * stag + dur
- await wait(exitTime * 0.3)
-
- containerRef.style.width = nextWidth + "px"
-
- const widthDur = 200
- await wait(widthDur * 0.3)
-
- setChars(containerRef, newText, "pre")
- containerRef.offsetWidth
-
- Array.from(containerRef.children).forEach((c) => (c.className = "cycle-char enter"))
- setCurrentText(newText)
- props.onValueChange?.(newText)
-
- const enterTime = getChars(newText).length * stag + dur
- await wait(enterTime)
-
- containerRef.style.width = ""
- isAnimating = false
- props.onAnimationEnd?.()
- }
-
- createEffect(
- on(
- () => props.value,
- (newValue) => {
- if (newValue !== currentText()) {
- animateToText(newValue)
- }
- },
- ),
- )
-
- const initRef = (el: HTMLSpanElement) => {
- containerRef = el
- setChars(el, props.value)
- }
-
- return (
- <span
- ref={initRef}
- class={`cycle-label ${props.class ?? ""}`}
- style={{
- "--c-dur": `${getDuration(currentText())}ms`,
- "--c-stag": `${stagger()}ms`,
- "--c-opacity-start": opacity()[0],
- "--c-opacity-end": opacity()[1],
- "--c-blur-start": `${blur()[0]}px`,
- "--c-blur-end": `${blur()[1]}px`,
- "--c-skew": `${skewX()}deg`,
- ...(typeof props.style === "object" ? props.style : {}),
- }}
- />
- )
-}
diff --git a/packages/ui/src/components/dialog.css b/packages/ui/src/components/dialog.css
index b788945dc..2e66b644f 100644
--- a/packages/ui/src/components/dialog.css
+++ b/packages/ui/src/components/dialog.css
@@ -5,16 +5,6 @@
inset: 0;
z-index: 50;
background-color: hsl(from var(--background-base) h s l / 0.2);
-
- animation: overlayHide var(--transition-duration) var(--transition-easing) forwards;
-
- &[data-expanded] {
- animation: overlayShow var(--transition-duration) var(--transition-easing) forwards;
- }
-
- @starting-style {
- animation: none;
- }
}
[data-component="dialog"] {
@@ -35,6 +25,7 @@
flex-direction: column;
align-items: center;
justify-items: start;
+ overflow: visible;
[data-slot="dialog-content"] {
display: flex;
@@ -44,8 +35,16 @@
width: 100%;
max-height: 100%;
min-height: 280px;
+ overflow: auto;
pointer-events: auto;
+ /* Hide scrollbar */
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
/* padding: 8px; */
/* padding: 8px 8px 0 8px; */
border-radius: var(--radius-xl);
@@ -53,16 +52,6 @@
background-clip: padding-box;
box-shadow: var(--shadow-lg-border-base);
- animation: contentHide var(--transition-duration) var(--transition-easing) forwards;
-
- &[data-expanded] {
- animation: contentShow var(--transition-duration) var(--transition-easing) forwards;
- }
-
- @starting-style {
- animation: none;
- }
-
[data-slot="dialog-header"] {
display: flex;
padding: 20px;
@@ -173,7 +162,7 @@
@keyframes contentShow {
from {
opacity: 0;
- transform: translateY(2.5%) scale(0.975);
+ transform: scale(0.98);
}
to {
opacity: 1;
@@ -187,6 +176,6 @@
}
to {
opacity: 0;
- transform: translateY(-2.5%) scale(0.975);
+ transform: scale(0.98);
}
}
diff --git a/packages/ui/src/components/dropdown-menu.css b/packages/ui/src/components/dropdown-menu.css
index 18266ac1a..cba041613 100644
--- a/packages/ui/src/components/dropdown-menu.css
+++ b/packages/ui/src/components/dropdown-menu.css
@@ -2,29 +2,26 @@
[data-component="dropdown-menu-sub-content"] {
min-width: 8rem;
overflow: hidden;
- border: none;
border-radius: var(--radius-md);
- box-shadow: var(--shadow-xs-border);
+ border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
background-clip: padding-box;
background-color: var(--surface-raised-stronger-non-alpha);
padding: 4px;
- z-index: 100;
+ box-shadow: var(--shadow-md);
+ z-index: 50;
transform-origin: var(--kb-menu-content-transform-origin);
- &:focus-within,
- &:focus {
+ &:focus,
+ &:focus-visible {
outline: none;
}
- animation: dropdownMenuContentHide var(--transition-duration) var(--transition-easing) forwards;
-
- @starting-style {
- animation: none;
+ &[data-closed] {
+ animation: dropdown-menu-close 0.15s ease-out;
}
&[data-expanded] {
- pointer-events: auto;
- animation: dropdownMenuContentShow var(--transition-duration) var(--transition-easing) forwards;
+ animation: dropdown-menu-open 0.15s ease-out;
}
}
@@ -41,22 +38,18 @@
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-base);
+ font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--text-strong);
- transition-property: background-color, color;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
- user-select: none;
-
- &:hover {
- background-color: var(--surface-raised-base-hover);
+ &[data-highlighted] {
+ background: var(--surface-raised-base-hover);
}
&[data-disabled] {
@@ -68,8 +61,6 @@
[data-slot="dropdown-menu-sub-trigger"] {
&[data-expanded] {
background: var(--surface-raised-base-hover);
- outline: none;
- border: none;
}
}
@@ -111,24 +102,24 @@
}
}
-@keyframes dropdownMenuContentShow {
+@keyframes dropdown-menu-open {
from {
opacity: 0;
- transform: scaleY(0.95);
+ transform: scale(0.96);
}
to {
opacity: 1;
- transform: scaleY(1);
+ transform: scale(1);
}
}
-@keyframes dropdownMenuContentHide {
+@keyframes dropdown-menu-close {
from {
opacity: 1;
- transform: scaleY(1);
+ transform: scale(1);
}
to {
opacity: 0;
- transform: scaleY(0.95);
+ transform: scale(0.96);
}
}
diff --git a/packages/ui/src/components/hover-card.css b/packages/ui/src/components/hover-card.css
index d23e43946..02d1f10ad 100644
--- a/packages/ui/src/components/hover-card.css
+++ b/packages/ui/src/components/hover-card.css
@@ -24,11 +24,11 @@
}
&[data-closed] {
- animation: hover-card-close var(--transition-duration) var(--transition-easing);
+ animation: hover-card-close 0.15s ease-out;
}
&[data-expanded] {
- animation: hover-card-open var(--transition-duration) var(--transition-easing);
+ animation: hover-card-open 0.15s ease-out;
}
[data-slot="hover-card-body"] {
diff --git a/packages/ui/src/components/icon-button.css b/packages/ui/src/components/icon-button.css
index f1371bfa9..aa550e990 100644
--- a/packages/ui/src/components/icon-button.css
+++ b/packages/ui/src/components/icon-button.css
@@ -7,9 +7,6 @@
user-select: none;
aspect-ratio: 1;
flex-shrink: 0;
- transition-property: background-color, color, opacity, box-shadow;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
&[data-variant="primary"] {
background-color: var(--icon-strong-base);
@@ -102,7 +99,7 @@
/* color: var(--icon-active); */
/* } */
}
- &[data-selected]:not(:disabled) {
+ &:selected:not(:disabled) {
background-color: var(--surface-raised-base-active);
/* [data-slot="icon-svg"] { */
/* color: var(--icon-selected); */
diff --git a/packages/ui/src/components/icon.css b/packages/ui/src/components/icon.css
index 467ff21bc..a2ebee30b 100644
--- a/packages/ui/src/components/icon.css
+++ b/packages/ui/src/components/icon.css
@@ -4,7 +4,7 @@
justify-content: center;
flex-shrink: 0;
/* resize: both; */
- aspect-ratio: 1 / 1;
+ aspect-ratio: 1/1;
color: var(--icon-base);
&[data-size="small"] {
diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx
index 97488a42f..544c6abdd 100644
--- a/packages/ui/src/components/icon.tsx
+++ b/packages/ui/src/components/icon.tsx
@@ -80,16 +80,13 @@ const icons = {
export interface IconProps extends ComponentProps<"svg"> {
name: keyof typeof icons
- size?: "small" | "normal" | "medium" | "large" | number
+ size?: "small" | "normal" | "medium" | "large"
}
export function Icon(props: IconProps) {
const [local, others] = splitProps(props, ["name", "size", "class", "classList"])
return (
- <div
- data-component="icon"
- data-size={typeof local.size !== "number" ? local.size || "normal" : `size-[${local.size}px]`}
- >
+ <div data-component="icon" data-size={local.size || "normal"}>
<svg
data-slot="icon-svg"
classList={{
diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css
index bba3519ab..b12d30415 100644
--- a/packages/ui/src/components/list.css
+++ b/packages/ui/src/components/list.css
@@ -19,7 +19,7 @@
[data-component="list"] {
display: flex;
flex-direction: column;
- gap: 8px;
+ gap: 12px;
overflow: hidden;
padding: 0 12px;
@@ -37,9 +37,7 @@
flex-shrink: 0;
background-color: transparent;
opacity: 0.5;
- transition-property: opacity;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition: opacity 0.15s ease;
&:hover:not(:disabled),
&:focus-visible:not(:disabled),
@@ -90,9 +88,7 @@
height: 20px;
background-color: transparent;
opacity: 0.5;
- transition-property: opacity;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition: opacity 0.15s ease;
&:hover:not(:disabled),
&:focus-visible:not(:disabled),
@@ -135,6 +131,15 @@
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;
+ }
[data-slot="list-empty-state"] {
display: flex;
@@ -210,9 +215,7 @@
background: linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha), transparent);
pointer-events: none;
opacity: 0;
- transition-property: opacity;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition: opacity 0.15s ease;
}
&[data-stuck="true"]::after {
@@ -248,22 +251,17 @@
align-items: center;
justify-content: center;
flex-shrink: 0;
- aspect-ratio: 1 / 1;
+ aspect-ratio: 1/1;
[data-component="icon"] {
color: var(--icon-strong-base);
}
}
-
- [name="check"] {
- color: var(--icon-strong-base);
- }
-
[data-slot="list-item-active-icon"] {
display: none;
align-items: center;
justify-content: center;
flex-shrink: 0;
- aspect-ratio: 1 / 1;
+ aspect-ratio: 1/1;
[data-component="icon"] {
color: var(--icon-strong-base);
}
diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx
index 15854180e..2132897f7 100644
--- a/packages/ui/src/components/list.tsx
+++ b/packages/ui/src/components/list.tsx
@@ -5,7 +5,6 @@ import { useI18n } from "../context/i18n"
import { Icon, type IconProps } from "./icon"
import { IconButton } from "./icon-button"
import { TextField } from "./text-field"
-import { ScrollFade } from "./scroll-fade"
function findByKey(container: HTMLElement, key: string) {
const nodes = container.querySelectorAll<HTMLElement>('[data-slot="list-item"][data-key]')
@@ -268,7 +267,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
{searchAction()}
</div>
</Show>
- <ScrollFade ref={setScrollRef} direction="vertical" fadeStartSize={0} fadeEndSize={20} data-slot="list-scroll">
+ <div ref={setScrollRef} data-slot="list-scroll">
<Show
when={flat().length > 0 || showAdd()}
fallback={
@@ -340,7 +339,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
</div>
</Show>
</Show>
- </ScrollFade>
+ </div>
</div>
)
}
diff --git a/packages/ui/src/components/logo.css b/packages/ui/src/components/logo.css
index 091649e8b..a909782b7 100644
--- a/packages/ui/src/components/logo.css
+++ b/packages/ui/src/components/logo.css
@@ -1,4 +1,4 @@
[data-component="logo-mark"] {
width: 16px;
- aspect-ratio: 4 / 5;
+ aspect-ratio: 4/5;
}
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 9c975d549..7aad01ace 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -49,7 +49,6 @@ import { Tooltip } from "./tooltip"
import { IconButton } from "./icon-button"
import { createAutoScroll } from "../hooks"
import { createResizeObserver } from "@solid-primitives/resize-observer"
-import { MorphChevron } from "./morph-chevron"
interface Diagnostic {
range: {
@@ -416,7 +415,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
toggleExpanded()
}}
>
- <MorphChevron expanded={expanded()} />
+ <Icon name="chevron-down" size="small" />
</button>
<div data-slot="user-message-copy-wrapper">
<Tooltip
diff --git a/packages/ui/src/components/morph-chevron.css b/packages/ui/src/components/morph-chevron.css
deleted file mode 100644
index f6edb3f64..000000000
--- a/packages/ui/src/components/morph-chevron.css
+++ /dev/null
@@ -1,10 +0,0 @@
-[data-slot="morph-chevron-svg"] {
- width: 16px;
- height: 16px;
- display: block;
- fill: none;
- stroke-width: 1.5;
- stroke: currentcolor;
- stroke-linecap: round;
- stroke-linejoin: round;
-}
diff --git a/packages/ui/src/components/morph-chevron.tsx b/packages/ui/src/components/morph-chevron.tsx
deleted file mode 100644
index 280aeb7e3..000000000
--- a/packages/ui/src/components/morph-chevron.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { createEffect, createUniqueId, on } from "solid-js"
-
-export interface MorphChevronProps {
- expanded: boolean
- class?: string
-}
-
-const COLLAPSED = "M4 6L8 10L12 6"
-const EXPANDED = "M4 10L8 6L12 10"
-
-export function MorphChevron(props: MorphChevronProps) {
- const id = createUniqueId()
- let path: SVGPathElement | undefined
- let expandAnim: SVGAnimateElement | undefined
- let collapseAnim: SVGAnimateElement | undefined
-
- createEffect(
- on(
- () => props.expanded,
- (expanded, prev) => {
- if (prev === undefined) {
- // Set initial state without animation
- path?.setAttribute("d", expanded ? EXPANDED : COLLAPSED)
- return
- }
- if (expanded) {
- expandAnim?.beginElement()
- } else {
- collapseAnim?.beginElement()
- }
- },
- ),
- )
-
- return (
- <svg
- viewBox="0 0 16 16"
- data-slot="morph-chevron-svg"
- class={props.class}
- xmlns="http://www.w3.org/2000/svg"
- aria-hidden="true"
- >
- <path ref={path} d={COLLAPSED} id={`morph-chevron-path-${id}`}>
- <animate
- ref={(el) => {
- expandAnim = el
- }}
- id={`morph-expand-${id}`}
- attributeName="d"
- dur="200ms"
- fill="freeze"
- calcMode="spline"
- keySplines="0.25 0 0.5 1"
- values="M4 6L8 10L12 6;M4 10L8 6L12 10"
- begin="indefinite"
- />
- <animate
- ref={(el) => {
- collapseAnim = el
- }}
- id={`morph-collapse-${id}`}
- attributeName="d"
- dur="200ms"
- fill="freeze"
- calcMode="spline"
- keySplines="0.25 0 0.5 1"
- values="M4 10L8 6L12 10;M4 6L8 10L12 6"
- begin="indefinite"
- />
- </path>
- </svg>
- )
-}
diff --git a/packages/ui/src/components/popover.css b/packages/ui/src/components/popover.css
index d200fe8b2..b49542afd 100644
--- a/packages/ui/src/components/popover.css
+++ b/packages/ui/src/components/popover.css
@@ -15,35 +15,16 @@
transform-origin: var(--kb-popover-content-transform-origin);
- animation: popoverContentHide var(--transition-duration) var(--transition-easing) forwards;
-
- @starting-style {
- animation: none;
- }
-
- &[data-expanded] {
- pointer-events: auto;
- animation: popoverContentShow var(--transition-duration) var(--transition-easing) forwards;
- }
-
- [data-origin-top-right] {
- transform-origin: top right;
- }
-
- [data-origin-top-left] {
- transform-origin: top left;
- }
-
- [data-origin-bottom-right] {
- transform-origin: bottom right;
+ &:focus-within {
+ outline: none;
}
- [data-origin-bottom-left] {
- transform-origin: bottom left;
+ &[data-closed] {
+ animation: popover-close 0.15s ease-out;
}
- &:focus-within {
- outline: none;
+ &[data-expanded] {
+ animation: popover-open 0.15s ease-out;
}
[data-slot="popover-header"] {
@@ -94,39 +75,24 @@
}
}
-@keyframes popoverContentShow {
+@keyframes popover-open {
from {
opacity: 0;
- transform: scaleY(0.95);
+ transform: scale(0.96);
}
to {
opacity: 1;
- transform: scaleY(1);
+ transform: scale(1);
}
}
-@keyframes popoverContentHide {
+@keyframes popover-close {
from {
opacity: 1;
- transform: scaleY(1);
+ transform: scale(1);
}
to {
opacity: 0;
- transform: scaleY(0.95);
- }
-}
-
-[data-component="model-popover-content"] {
- transform-origin: var(--kb-popper-content-transform-origin);
- pointer-events: none;
- animation: popoverContentHide var(--transition-duration) var(--transition-easing) forwards;
-
- @starting-style {
- animation: none;
- }
-
- &[data-expanded] {
- pointer-events: auto;
- animation: popoverContentShow var(--transition-duration) var(--transition-easing) forwards;
+ transform: scale(0.96);
}
}
diff --git a/packages/ui/src/components/progress-circle.css b/packages/ui/src/components/progress-circle.css
index d8dc4e1d0..afaf72af6 100644
--- a/packages/ui/src/components/progress-circle.css
+++ b/packages/ui/src/components/progress-circle.css
@@ -1,10 +1,12 @@
[data-component="progress-circle"] {
- color: inherit;
+ transform: rotate(-90deg);
[data-slot="progress-circle-background"] {
- transform-origin: 50% 50%;
- transform: rotate(270deg);
- stroke-opacity: 0.5;
+ stroke: var(--border-weak-base);
+ }
+
+ [data-slot="progress-circle-progress"] {
+ stroke: var(--border-active);
transition: stroke-dashoffset 0.35s cubic-bezier(0.65, 0, 0.35, 1);
}
}
diff --git a/packages/ui/src/components/progress-circle.tsx b/packages/ui/src/components/progress-circle.tsx
index 40d1e2022..02bd36bb7 100644
--- a/packages/ui/src/components/progress-circle.tsx
+++ b/packages/ui/src/components/progress-circle.tsx
@@ -1,4 +1,4 @@
-import { type ComponentProps, splitProps } from "solid-js"
+import { type ComponentProps, createMemo, splitProps } from "solid-js"
export interface ProgressCircleProps extends Pick<ComponentProps<"svg">, "class" | "classList"> {
percentage: number
@@ -9,15 +9,26 @@ export interface ProgressCircleProps extends Pick<ComponentProps<"svg">, "class"
export function ProgressCircle(props: ProgressCircleProps) {
const [split, rest] = splitProps(props, ["percentage", "size", "strokeWidth", "class", "classList"])
- const size = () => split.size || 18
- const r = 7
+ const size = () => split.size || 16
+ const strokeWidth = () => split.strokeWidth || 3
+
+ const viewBoxSize = 16
+ const center = viewBoxSize / 2
+ const radius = () => center - strokeWidth() / 2
+ const circumference = createMemo(() => 2 * Math.PI * radius())
+
+ const offset = createMemo(() => {
+ const clampedPercentage = Math.max(0, Math.min(100, split.percentage || 0))
+ const progress = clampedPercentage / 100
+ return circumference() * (1 - progress)
+ })
return (
<svg
{...rest}
width={size()}
height={size()}
- viewBox="0 0 18 18"
+ viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
fill="none"
data-component="progress-circle"
classList={{
@@ -25,18 +36,21 @@ export function ProgressCircle(props: ProgressCircleProps) {
[split.class ?? ""]: !!split.class,
}}
>
- <circle cx="9" cy="9" r="7.75" stroke="currentColor" stroke-width="1.5" />
- <path
- opacity="0.5"
- d={(() => {
- const pct = Math.min(100, Math.max(0, split.percentage))
- const angle = (pct / 100) * 2 * Math.PI - Math.PI / 2
- const x = 9 + r * Math.cos(angle)
- const y = 9 + r * Math.sin(angle)
- const largeArc = pct > 50 ? 1 : 0
- return `M9 2A${r} ${r} 0 ${largeArc} 1 ${x} ${y}L9 9Z`
- })()}
- fill="currentColor"
+ <circle
+ cx={center}
+ cy={center}
+ r={radius()}
+ data-slot="progress-circle-background"
+ stroke-width={strokeWidth()}
+ />
+ <circle
+ cx={center}
+ cy={center}
+ r={radius()}
+ data-slot="progress-circle-progress"
+ stroke-width={strokeWidth()}
+ stroke-dasharray={circumference().toString()}
+ stroke-dashoffset={offset()}
/>
</svg>
)
diff --git a/packages/ui/src/components/radio-group.css b/packages/ui/src/components/radio-group.css
index df51fc8e8..3d672bb30 100644
--- a/packages/ui/src/components/radio-group.css
+++ b/packages/ui/src/components/radio-group.css
@@ -27,9 +27,12 @@
content: "";
opacity: var(--indicator-opacity, 1);
position: absolute;
- transition-property: opacity, box-shadow, width, height, transform;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition:
+ opacity 300ms ease-in-out,
+ box-shadow 100ms ease-in-out,
+ width 150ms ease,
+ height 150ms ease,
+ transform 150ms ease;
}
[data-slot="radio-group-item"] {
@@ -43,9 +46,7 @@
content: "";
inset: 6px 0;
position: absolute;
- transition-property: opacity;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition: opacity 150ms ease;
width: 1px;
transform: translateX(-0.5px);
}
@@ -71,9 +72,9 @@
padding: 6px 12px;
place-content: center;
position: relative;
+ transition-duration: 150ms;
transition-property: color, opacity;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition-timing-function: ease-in-out;
user-select: none;
}
diff --git a/packages/ui/src/components/reasoning-icon.css b/packages/ui/src/components/reasoning-icon.css
deleted file mode 100644
index b03f69a93..000000000
--- a/packages/ui/src/components/reasoning-icon.css
+++ /dev/null
@@ -1,8 +0,0 @@
-[data-component="reasoning-icon"] {
- color: var(--icon-strong-base);
-
- [data-slot="reasoning-icon-percentage"] {
- transition: clip-path 200ms cubic-bezier(0.25, 0, 0.5, 1);
- clip-path: inset(calc(100% - var(--reasoning-icon-percentage) * 100%) 0 0 0);
- }
-}
diff --git a/packages/ui/src/components/reasoning-icon.tsx b/packages/ui/src/components/reasoning-icon.tsx
deleted file mode 100644
index 7bac49ffd..000000000
--- a/packages/ui/src/components/reasoning-icon.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { type ComponentProps, splitProps } from "solid-js"
-
-export interface ReasoningIconProps extends Pick<ComponentProps<"svg">, "class" | "classList"> {
- percentage: number
- size?: number
- strokeWidth?: number
-}
-
-export function ReasoningIcon(props: ReasoningIconProps) {
- const [split, rest] = splitProps(props, ["percentage", "size", "strokeWidth", "class", "classList"])
-
- const size = () => split.size || 16
- const strokeWidth = () => split.strokeWidth || 1.25
-
- return (
- <svg
- {...rest}
- width={size()}
- height={size()}
- viewBox={`0 0 16 16`}
- fill="none"
- data-component="reasoning-icon"
- classList={{
- ...(split.classList ?? {}),
- [split.class ?? ""]: !!split.class,
- }}
- >
- <path
- d="M5.83196 10.3225V11.1666C5.83196 11.7189 6.27967 12.1666 6.83196 12.1666H9.16687C9.71915 12.1666 10.1669 11.7189 10.1669 11.1666V10.3225M5.83196 10.3225C5.55695 10.1843 5.29695 10.0206 5.05505 9.83459C3.90601 8.95086 3.16549 7.56219 3.16549 6.00055C3.16549 3.33085 5.32971 1.16663 7.99941 1.16663C10.6691 1.16663 12.8333 3.33085 12.8333 6.00055C12.8333 7.56219 12.0928 8.95086 10.9438 9.83459C10.7019 10.0206 10.4419 10.1843 10.1669 10.3225M5.83196 10.3225H10.1669M6.5 14.1666H9.5"
- stroke="currentColor"
- stroke-linecap="round"
- stroke-linejoin="round"
- />
- <circle
- cx="8"
- cy="5.83325"
- r="2.86364"
- fill="currentColor"
- stroke="currentColor"
- stroke-width={strokeWidth()}
- style={{ "--reasoning-icon-percentage": split.percentage / 100 }}
- data-slot="reasoning-icon-percentage"
- />
- </svg>
- )
-}
diff --git a/packages/ui/src/components/resize-handle.css b/packages/ui/src/components/resize-handle.css
index 0aad9b967..c309ff838 100644
--- a/packages/ui/src/components/resize-handle.css
+++ b/packages/ui/src/components/resize-handle.css
@@ -6,9 +6,7 @@
content: "";
position: absolute;
opacity: 0;
- transition-property: opacity;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition: opacity 0.15s ease-in-out;
}
&:hover::after,
diff --git a/packages/ui/src/components/scroll-fade.css b/packages/ui/src/components/scroll-fade.css
deleted file mode 100644
index ede5fabec..000000000
--- a/packages/ui/src/components/scroll-fade.css
+++ /dev/null
@@ -1,82 +0,0 @@
-[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;
-
- &[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);
- }
- }
-}
diff --git a/packages/ui/src/components/scroll-fade.tsx b/packages/ui/src/components/scroll-fade.tsx
deleted file mode 100644
index 0effb678a..000000000
--- a/packages/ui/src/components/scroll-fade.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-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()
-
- let rafId: number | undefined
- let isPolling = false
- let pollTimeout: ReturnType<typeof setTimeout> | undefined
-
- const startPolling = () => {
- if (isPolling) return
- isPolling = true
-
- const pollScroll = () => {
- updateFade()
- rafId = requestAnimationFrame(pollScroll)
- }
- rafId = requestAnimationFrame(pollScroll)
- }
-
- const stopPolling = () => {
- if (!isPolling) return
- isPolling = false
- if (rafId !== undefined) {
- cancelAnimationFrame(rafId)
- rafId = undefined
- }
- }
-
- const schedulePollingStop = () => {
- if (pollTimeout !== undefined) clearTimeout(pollTimeout)
- pollTimeout = setTimeout(stopPolling, 1000)
- }
-
- const onActivity = () => {
- updateFade()
- if (local.trackTransformSelector) {
- startPolling()
- schedulePollingStop()
- }
- }
-
- containerRef.addEventListener("scroll", onActivity, { passive: true })
-
- const resizeObserver = new ResizeObserver(() => {
- lastScrollSize = 0
- lastClientSize = 0
- onActivity()
- })
- resizeObserver.observe(containerRef)
-
- const mutationObserver = new MutationObserver(() => {
- lastScrollSize = 0
- lastClientSize = 0
- requestAnimationFrame(onActivity)
- })
- mutationObserver.observe(containerRef, {
- childList: true,
- subtree: true,
- characterData: true,
- })
-
- onCleanup(() => {
- containerRef?.removeEventListener("scroll", onActivity)
- resizeObserver.disconnect()
- mutationObserver.disconnect()
- stopPolling()
- if (pollTimeout !== undefined) clearTimeout(pollTimeout)
- })
- })
-
- 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>
- )
-}
diff --git a/packages/ui/src/components/scroll-reveal.tsx b/packages/ui/src/components/scroll-reveal.tsx
deleted file mode 100644
index 6e5072dc8..000000000
--- a/packages/ui/src/components/scroll-reveal.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import { type JSX, onCleanup, splitProps } from "solid-js"
-import { ScrollFade, type ScrollFadeProps } from "./scroll-fade"
-
-const SCROLL_SPEED = 60
-const PAUSE_DURATION = 800
-
-type 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"> {
- 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>
- )
-}
diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css
index 5c7969883..25dd2eb40 100644
--- a/packages/ui/src/components/select.css
+++ b/packages/ui/src/components/select.css
@@ -1,13 +1,7 @@
[data-component="select"] {
[data-slot="select-select-trigger"] {
- display: flex;
- padding: 4px 8px !important;
- align-items: center;
- justify-content: space-between;
+ padding: 0 4px 0 8px;
box-shadow: none;
- transition-property: background-color;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
[data-slot="select-select-trigger-value"] {
overflow: hidden;
@@ -21,10 +15,10 @@
align-items: center;
justify-content: center;
flex-shrink: 0;
- color: var(--icon-base);
+ color: var(--text-weak);
+ transition: transform 0.1s ease-in-out;
}
- &:hover,
&[data-expanded] {
&[data-variant="secondary"] {
background-color: var(--button-secondary-hover);
@@ -36,42 +30,78 @@
background-color: var(--icon-strong-active);
}
}
- &:not([data-expanded]):focus,
+
&:not([data-expanded]):focus-visible {
&[data-variant="secondary"] {
background-color: var(--button-secondary-base);
}
&[data-variant="ghost"] {
- background-color: transparent;
+ background-color: var(--surface-raised-base-hover);
}
&[data-variant="primary"] {
background-color: var(--icon-strong-base);
}
}
}
+
+ &[data-trigger-style="settings"] {
+ [data-slot="select-select-trigger"] {
+ padding: 6px 6px 6px 12px;
+ box-shadow: none;
+ border-radius: 6px;
+ min-width: 160px;
+ height: 32px;
+ justify-content: flex-end;
+ gap: 12px;
+ background-color: transparent;
+
+ [data-slot="select-select-trigger-value"] {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-regular);
+ }
+ [data-slot="select-select-trigger-icon"] {
+ width: 16px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ color: var(--text-weak);
+ background-color: var(--surface-raised-base);
+ border-radius: 4px;
+ transition: transform 0.1s ease-in-out;
+ }
+
+ &[data-slot="select-select-trigger"]:hover:not(:disabled),
+ &[data-slot="select-select-trigger"][data-expanded],
+ &[data-slot="select-select-trigger"][data-expanded]:hover:not(:disabled) {
+ background-color: var(--input-base);
+ box-shadow: var(--shadow-xs-border-base);
+ }
+
+ &:not([data-expanded]):focus {
+ background-color: transparent;
+ box-shadow: none;
+ }
+ }
+ }
}
[data-component="select-content"] {
- min-width: 8rem;
+ min-width: 104px;
max-width: 23rem;
overflow: hidden;
border-radius: var(--radius-md);
background-color: var(--surface-raised-stronger-non-alpha);
padding: 4px;
box-shadow: var(--shadow-xs-border);
- z-index: 50;
- transform-origin: var(--kb-popper-content-transform-origin);
- pointer-events: none;
-
- animation: selectContentHide var(--transition-duration) var(--transition-easing) forwards;
-
- @starting-style {
- animation: none;
- }
+ z-index: 60;
&[data-expanded] {
- pointer-events: auto;
- animation: selectContentShow var(--transition-duration) var(--transition-easing) forwards;
+ animation: select-open 0.15s ease-out;
}
[data-slot="select-select-content-list"] {
@@ -81,38 +111,43 @@
overflow-x: hidden;
display: flex;
flex-direction: column;
+
&:focus {
outline: none;
}
+
> *:not([role="presentation"]) + *:not([role="presentation"]) {
margin-top: 2px;
}
}
+
[data-slot="select-select-item"] {
position: relative;
display: flex;
align-items: center;
- padding: 4px 8px;
+ padding: 2px 8px;
gap: 12px;
- border-radius: var(--radius-sm);
+ border-radius: 4px;
+ cursor: default;
/* text-12-medium */
font-family: var(--font-family-sans);
- font-size: var(--font-size-base);
+ font-size: var(--font-size-small);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large); /* 166.667% */
letter-spacing: var(--letter-spacing-normal);
+
color: var(--text-strong);
- transition-property: background-color, color;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition:
+ background-color 0.2s ease-in-out,
+ color 0.2s ease-in-out;
outline: none;
user-select: none;
- &:hover {
- background-color: var(--surface-raised-base-hover);
+ &[data-highlighted] {
+ background: var(--surface-raised-base-hover);
}
&[data-disabled] {
background-color: var(--surface-raised-base);
@@ -125,11 +160,6 @@
margin-left: auto;
width: 16px;
height: 16px;
- color: var(--icon-strong-base);
-
- svg {
- color: var(--icon-strong-base);
- }
}
&:focus {
outline: none;
@@ -140,24 +170,33 @@
}
}
-@keyframes selectContentShow {
- from {
- opacity: 0;
- transform: scaleY(0.95);
+[data-component="select-content"][data-trigger-style="settings"] {
+ min-width: 160px;
+ border-radius: 8px;
+ padding: 0;
+
+ [data-slot="select-select-content-list"] {
+ padding: 4px;
}
- to {
- opacity: 1;
- transform: scaleY(1);
+
+ [data-slot="select-select-item"] {
+ /* text-14-regular */
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-base);
+ font-style: normal;
+ font-weight: var(--font-weight-regular);
+ line-height: var(--line-height-large);
+ letter-spacing: var(--letter-spacing-normal);
}
}
-@keyframes selectContentHide {
+@keyframes select-open {
from {
- opacity: 1;
- transform: scaleY(1);
+ opacity: 0;
+ transform: scale(0.95);
}
to {
- opacity: 0;
- transform: scaleY(0.95);
+ opacity: 1;
+ transform: scale(1);
}
}
diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx
index fef00500a..0386c329e 100644
--- a/packages/ui/src/components/select.tsx
+++ b/packages/ui/src/components/select.tsx
@@ -1,10 +1,8 @@
import { Select as Kobalte } from "@kobalte/core/select"
-import { createMemo, createSignal, onCleanup, splitProps, type ComponentProps, type JSX } from "solid-js"
+import { createMemo, onCleanup, splitProps, type ComponentProps, type JSX } from "solid-js"
import { pipe, groupBy, entries, map } from "remeda"
-import { Show } from "solid-js"
import { Button, ButtonProps } from "./button"
import { Icon } from "./icon"
-import { MorphChevron } from "./morph-chevron"
export type SelectProps<T> = Omit<ComponentProps<typeof Kobalte<T>>, "value" | "onSelect" | "children"> & {
placeholder?: string
@@ -40,8 +38,6 @@ export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">)
"triggerVariant",
])
- const [isOpen, setIsOpen] = createSignal(false)
-
const state = {
key: undefined as string | undefined,
cleanup: undefined as (() => void) | void,
@@ -89,7 +85,7 @@ export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">)
data-component="select"
data-trigger-style={local.triggerVariant}
placement={local.triggerVariant === "settings" ? "bottom-end" : "bottom-start"}
- gutter={8}
+ gutter={4}
value={local.current}
options={grouped()}
optionValue={(x) => (local.value ? local.value(x) : (x as string))}
@@ -119,7 +115,7 @@ export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">)
: (itemProps.item.rawValue as string)}
</Kobalte.ItemLabel>
<Kobalte.ItemIndicator data-slot="select-select-item-indicator">
- <Icon name="check" size="small" />
+ <Icon name="check-small" size="small" />
</Kobalte.ItemIndicator>
</Kobalte.Item>
)}
@@ -128,7 +124,6 @@ export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">)
stop()
}}
onOpenChange={(open) => {
- setIsOpen(open)
local.onOpenChange?.(open)
if (!open) stop()
}}
@@ -154,12 +149,7 @@ export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">)
}}
</Kobalte.Value>
<Kobalte.Icon data-slot="select-select-trigger-icon">
- <Show when={local.triggerVariant === "settings"}>
- <Icon name="selector" size="small" />
- </Show>
- <Show when={local.triggerVariant !== "settings"}>
- <MorphChevron expanded={isOpen()} />
- </Show>
+ <Icon name={local.triggerVariant === "settings" ? "selector" : "chevron-down"} size="small" />
</Kobalte.Icon>
</Kobalte.Trigger>
<Kobalte.Portal>
diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css
index 4fc88b199..20d2fef15 100644
--- a/packages/ui/src/components/session-review.css
+++ b/packages/ui/src/components/session-review.css
@@ -63,8 +63,12 @@
[data-slot="accordion-item"] {
[data-slot="accordion-content"] {
- /* Use grid-template-rows for smooth height transition */
- display: grid;
+ display: none;
+ }
+ &[data-expanded] {
+ [data-slot="accordion-content"] {
+ display: block;
+ }
}
}
@@ -126,9 +130,7 @@
cursor: pointer;
border-radius: 4px;
opacity: 0;
- transition-property: opacity, background-color, color;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition: opacity 0.15s ease;
&:hover {
color: var(--text-strong);
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index b5a359707..84ec934e2 100644
--- a/packages/ui/src/components/session-review.tsx
+++ b/packages/ui/src/components/session-review.tsx
@@ -290,8 +290,8 @@ export const SessionReview = (props: SessionReviewProps) => {
<div data-slot="session-review-title">{i18n.t("ui.sessionReview.title")}</div>
<div data-slot="session-review-actions">
<Show when={props.onDiffStyleChange}>
- <RadioGroup<SessionReviewDiffStyle>
- options={["unified", "split"]}
+ <RadioGroup
+ options={["unified", "split"] as const}
current={diffStyle()}
value={(style) => style}
label={(style) =>
@@ -501,7 +501,6 @@ export const SessionReview = (props: SessionReviewProps) => {
value={diff.file}
id={diffId(diff.file)}
data-file={diff.file}
- expanded={open().includes(diff.file)}
data-slot="session-review-accordion-item"
data-selected={props.focusedFile === diff.file ? "" : undefined}
>
diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css
index 088b377cb..d1ade879e 100644
--- a/packages/ui/src/components/session-turn.css
+++ b/packages/ui/src/components/session-turn.css
@@ -102,11 +102,10 @@
[data-component="user-message"] [data-slot="user-message-text"] {
max-height: var(--user-message-collapsed-height, 64px);
- transition: max-height 200ms cubic-bezier(0.25, 0, 0.5, 1);
}
[data-component="user-message"][data-expanded="true"] [data-slot="user-message-text"] {
- max-height: 2000px;
+ max-height: none;
}
[data-component="user-message"][data-can-expand="true"] [data-slot="user-message-text"] {
@@ -152,6 +151,17 @@
background: transparent;
cursor: pointer;
color: var(--text-weak);
+
+ [data-slot="icon-svg"] {
+ transition: transform 0.15s ease;
+ }
+ }
+
+ [data-component="user-message"][data-expanded="true"]
+ [data-slot="user-message-text"]
+ [data-slot="user-message-expand"]
+ [data-slot="icon-svg"] {
+ transform: rotate(180deg);
}
[data-component="user-message"] [data-slot="user-message-text"] [data-slot="user-message-expand"]:hover {
@@ -457,7 +467,6 @@
gap: 16px;
align-items: center;
justify-content: flex-end;
- color: var(--icon-base);
}
[data-slot="session-turn-accordion-content"] {
diff --git a/packages/ui/src/components/switch.css b/packages/ui/src/components/switch.css
index 9ea722760..89e844732 100644
--- a/packages/ui/src/components/switch.css
+++ b/packages/ui/src/components/switch.css
@@ -26,9 +26,9 @@
border-radius: 3px;
border: 1px solid var(--border-weak-base);
background: var(--surface-base);
- transition-property: background-color, border-color, box-shadow;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition:
+ background-color 150ms,
+ border-color 150ms;
}
[data-slot="switch-thumb"] {
@@ -47,9 +47,9 @@
0 1px 3px 0 rgba(19, 16, 16, 0.08);
transform: translateX(-1px);
- transition-property: transform, background-color, border-color;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
+ transition:
+ transform 150ms,
+ background-color 150ms;
}
[data-slot="switch-label"] {
diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css
index dab07dab9..56c3e083f 100644
--- a/packages/ui/src/components/tabs.css
+++ b/packages/ui/src/components/tabs.css
@@ -58,9 +58,6 @@
border-bottom: 1px solid var(--border-weak-base);
border-right: 1px solid var(--border-weak-base);
background-color: var(--background-base);
- transition-property: background-color, border-color, color;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
[data-slot="tabs-trigger"] {
display: flex;
diff --git a/packages/ui/src/components/tag.css b/packages/ui/src/components/tag.css
index 5ffd2b911..0e8b7b9f1 100644
--- a/packages/ui/src/components/tag.css
+++ b/packages/ui/src/components/tag.css
@@ -8,9 +8,6 @@
border: 0.5px solid var(--border-weak-base);
background: var(--surface-raised-base);
color: var(--text-base);
- transition-property: background-color, border-color, color;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
&[data-size="normal"] {
height: 18px;
diff --git a/packages/ui/src/context/dialog.tsx b/packages/ui/src/context/dialog.tsx
index 413212512..afba5f648 100644
--- a/packages/ui/src/context/dialog.tsx
+++ b/packages/ui/src/context/dialog.tsx
@@ -28,7 +28,6 @@ const Context = createContext<ReturnType<typeof init>>()
function init() {
const [active, setActive] = createSignal<Active | undefined>()
- const [renders, setRenders] = createSignal<Record<string, JSX.Element>>({})
const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined }
const lock = { value: false }
@@ -119,32 +118,12 @@ function init() {
setActive({ id, node, dispose, owner, onClose, setClosing })
}
- const render = (element: JSX.Element, id: string, owner: Owner) => {
- setRenders((renders) => ({ ...renders, [id]: element }))
- show(
- () => element,
- owner,
- () => {
- setRenders((renders) => {
- const { [id]: _, ...rest } = renders
- return rest
- })
- },
- )
- }
-
- const isActive = (id: string) => {
- return renders()[id] !== undefined
- }
-
return {
get active() {
return active()
},
- isActive,
close,
show,
- render,
}
}
@@ -173,17 +152,10 @@ export function useDialog() {
get active() {
return ctx.active
},
- isActive(id: string) {
- return ctx.isActive(id)
- },
show(element: DialogElement, onClose?: () => void) {
const base = ctx.active?.owner ?? owner
ctx.show(element, base, onClose)
},
- render(element: JSX.Element, id: string) {
- const base = ctx.active?.owner ?? owner
- ctx.render(element, id, base)
- },
close() {
ctx.close()
},
diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css
index 7c8548734..3ed0310ef 100644
--- a/packages/ui/src/styles/index.css
+++ b/packages/ui/src/styles/index.css
@@ -33,10 +33,8 @@
@import "../components/markdown.css" layer(components);
@import "../components/message-part.css" layer(components);
@import "../components/message-nav.css" layer(components);
-@import "../components/morph-chevron.css" layer(components);
@import "../components/popover.css" layer(components);
@import "../components/progress-circle.css" layer(components);
-@import "../components/reasoning-icon.css" layer(components);
@import "../components/radio-group.css" layer(components);
@import "../components/resize-handle.css" layer(components);
@import "../components/select.css" layer(components);
diff --git a/packages/ui/src/styles/utilities.css b/packages/ui/src/styles/utilities.css
index 82a913c88..8c954f1fe 100644
--- a/packages/ui/src/styles/utilities.css
+++ b/packages/ui/src/styles/utilities.css
@@ -1,17 +1,6 @@
:root {
interpolate-size: allow-keywords;
- /* Transition tokens */
- --transition-duration: 200ms;
- --transition-easing: cubic-bezier(0.25, 0, 0.5, 1);
- --transition-fast: 150ms;
- --transition-slow: 300ms;
-
- /* Allow height transitions from 0 to auto */
- @supports (interpolate-size: allow-keywords) {
- interpolate-size: allow-keywords;
- }
-
[data-popper-positioner] {
pointer-events: none;
}
@@ -140,34 +129,3 @@
line-height: var(--line-height-x-large); /* 120% */
letter-spacing: var(--letter-spacing-tightest);
}
-
-/* Transition utility classes */
-.transition-colors {
- transition-property: background-color, border-color, color, fill, stroke;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
-}
-
-.transition-opacity {
- transition-property: opacity;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
-}
-
-.transition-transform {
- transition-property: transform;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
-}
-
-.transition-shadow {
- transition-property: box-shadow;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
-}
-
-.transition-interactive {
- transition-property: background-color, border-color, color, box-shadow, opacity;
- transition-duration: var(--transition-duration);
- transition-timing-function: var(--transition-easing);
-}