diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 4d96c6c5f660..4f0dcc3ee65e 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(props: { provider?: string - children?: JSX.Element | ((open: boolean) => JSX.Element) + children?: JSX.Element triggerAs?: T triggerProps?: ComponentProps }) { @@ -182,13 +182,12 @@ export function ModelSelectorPopover(props: { as={props.triggerAs ?? "div"} {...(props.triggerProps as any)} > - {typeof props.children === "function" ? props.children(store.open) : props.children} + {props.children} 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 6e614eb1d7ef..5c1d417eb081 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 = (props) => { clearInput() client.session .shell({ - sessionID: session?.id || "", + sessionID: session.id, agent, model, command: text, @@ -1278,7 +1275,7 @@ export const PromptInput: Component = (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 = (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 = (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 = (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 = (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 = (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 = (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 = (props) => { restoreInput() } - pending.set(session?.id || "", { abort: controller, cleanup }) + pending.set(session.id, { abort: controller, cleanup }) const abort = new Promise>>((resolve) => { if (controller.signal.aborted) { @@ -1575,7 +1572,7 @@ export const PromptInput: Component = (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 = (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 = (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 = (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 (
@@ -1693,7 +1668,7 @@ export const PromptInput: Component = (props) => { } > - + @{(item as { type: "agent"; name: string }).name} @@ -1754,9 +1729,9 @@ export const PromptInput: Component = (props) => { }} > -
+
- + {language.t("prompt.dropzone.label")}
@@ -1795,7 +1770,7 @@ export const PromptInput: Component = (props) => { }} >
- +
{getFilenameTruncated(item.path, 14)} @@ -1812,7 +1787,7 @@ export const PromptInput: Component = (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 = (props) => { when={attachment.mime.startsWith("image/")} fallback={
- +
} > @@ -1916,7 +1891,7 @@ export const PromptInput: Component = (props) => {
-
+
@@ -1947,17 +1922,12 @@ export const PromptInput: Component = (props) => { title={language.t("command.model.choose")} keybind={command.keybind("model.choose")} > - } @@ -1968,15 +1938,11 @@ export const PromptInput: Component = (props) => { keybind={command.keybind("model.choose")} > - {(open) => ( - <> - - - - {local.model.current()?.name ?? language.t("dialog.model.select.title")} - - - )} + + + + {local.model.current()?.name ?? language.t("dialog.model.select.title")} + @@ -1989,13 +1955,10 @@ export const PromptInput: Component = (props) => { @@ -2009,7 +1972,7 @@ export const PromptInput: Component = (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 = (props) => {
-
+
= (props) => { e.currentTarget.value = "" }} /> -
+
@@ -2074,7 +2036,7 @@ export const PromptInput: Component = (props) => {
{language.t("prompt.action.send")} - +
@@ -2085,7 +2047,7 @@ export const PromptInput: Component = (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")} /> diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index 92b060212d66..1e37d8f6a201 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 = () => ( -
- +
+
) @@ -101,7 +101,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
) } diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index efd18c8bd6b3..393da0c2ab05 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 ( - +
@@ -435,6 +429,6 @@ export const SettingsKeybinds: Component = () => {
- +
) } diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index d16416c9f2d7..29ac86f29a45 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 b310eeedb64c..7bf287fe549c 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 e30be95e056e..535d38e3d076 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 {} export interface AccordionItemProps extends ComponentProps {} @@ -9,8 +8,6 @@ export interface AccordionHeaderProps extends ComponentProps {} export interface AccordionContentProps extends ComponentProps {} -const AccordionItemContext = createContext>() - 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 ( - split.expanded ?? false}> - - + ) } @@ -89,25 +84,9 @@ function AccordionContent(props: ParentProps) { ) } -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 ( -
- -
- ) -} - 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 02a7ade712a2..d9b345923047 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 b2d2004d3c8c..7f974b2f76e7 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, - Pick, "class" | "classList" | "children" | "style"> { + Pick, "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 809fbdacde37..6dae47223d67 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 cad0dd2dd6bd..b10ebbbd197b 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 cc62b2b87c54..1f20cf85d9a6 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 55b7b60333c9..903afc3085ee 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>() +import { ComponentProps, ParentProps, splitProps } from "solid-js" +import { Icon } from "./icon" export interface CollapsibleProps extends ParentProps { class?: string @@ -11,30 +9,17 @@ export interface CollapsibleProps extends ParentProps { } 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 ( - - - {local.children} - - + ) } @@ -47,10 +32,9 @@ function CollapsibleContent(props: ComponentProps) { } function CollapsibleArrow(props?: ComponentProps<"div">) { - const isOpen = useContext(CollapsibleContext) return (
- +
) } diff --git a/packages/ui/src/components/cycle-label.css b/packages/ui/src/components/cycle-label.css deleted file mode 100644 index e3b5256d4fd8..000000000000 --- 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 e34385a2c2f8..000000000000 --- 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 { - 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 ( - - ) -} diff --git a/packages/ui/src/components/dialog.css b/packages/ui/src/components/dialog.css index b788945dceb6..2e66b644fc90 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 18266ac1a1c7..cba041613eab 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 d23e43946dac..02d1f10ad1d9 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 f1371bfa9aef..aa550e990f9a 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 467ff21bcc5d..a2ebee30bc1c 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 97488a42f0fb..544c6abdd214 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 ( -
+
('[data-slot="list-item"][data-key]') @@ -268,7 +267,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) {searchAction()}
- +
0 || showAdd()} fallback={ @@ -340,7 +339,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void })
-
+
) } diff --git a/packages/ui/src/components/logo.css b/packages/ui/src/components/logo.css index 091649e8bb68..a909782b7708 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 9c975d54906b..7aad01acea38 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() }} > - +
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 ( - - ) -} diff --git a/packages/ui/src/components/popover.css b/packages/ui/src/components/popover.css index d200fe8b2476..b49542afd9b8 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 d8dc4e1d05fa..afaf72af61ee 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 40d1e2022f09..02bd36bb7105 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, "class" | "classList"> { percentage: number @@ -9,15 +9,26 @@ export interface ProgressCircleProps extends Pick, "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 ( - - { - 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" + + ) diff --git a/packages/ui/src/components/radio-group.css b/packages/ui/src/components/radio-group.css index df51fc8e86eb..3d672bb300d5 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 b03f69a931ae..000000000000 --- 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 7bac49ffd2ef..000000000000 --- 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, "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 ( - - - - - ) -} diff --git a/packages/ui/src/components/resize-handle.css b/packages/ui/src/components/resize-handle.css index 0aad9b96781d..c309ff838bc4 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 ede5fabec450..000000000000 --- 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 0effb678a525..000000000000 --- 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 { - 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 | 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 ( -
{ - 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} -
- ) -} diff --git a/packages/ui/src/components/scroll-reveal.tsx b/packages/ui/src/components/scroll-reveal.tsx deleted file mode 100644 index 6e5072dc81e5..000000000000 --- 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 { - 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 | undefined - let scrollAnimationState: ScrollAnimationState | null = null - - const handleMouseEnter: JSX.EventHandler = () => { - 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 = () => { - if (hoverTimeout) { - clearTimeout(hoverTimeout) - hoverTimeout = undefined - } - stopScrollAnimation(scrollAnimationState, containerRef) - scrollAnimationState = null - } - - onCleanup(() => { - if (hoverTimeout) { - clearTimeout(hoverTimeout) - } - stopScrollAnimation(scrollAnimationState, containerRef) - }) - - return ( - { - containerRef = el - local.ref?.(el) - }} - fadeStartSize={8} - fadeEndSize={8} - direction="horizontal" - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - {...others} - > - {local.children} - - ) -} diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index 5c79698835e4..25dd2eb40b67 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 fef00500a716..0386c329ec4b 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 = Omit>, "value" | "onSelect" | "children"> & { placeholder?: string @@ -40,8 +38,6 @@ export function Select(props: SelectProps & Omit) "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(props: SelectProps & Omit) 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(props: SelectProps & Omit) : (itemProps.item.rawValue as string)} - + )} @@ -128,7 +124,6 @@ export function Select(props: SelectProps & Omit) stop() }} onOpenChange={(open) => { - setIsOpen(open) local.onOpenChange?.(open) if (!open) stop() }} @@ -154,12 +149,7 @@ export function Select(props: SelectProps & Omit) }} - - - - - - + diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index 4fc88b1994c2..20d2fef15292 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 b5a359707c6c..84ec934e24da 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) => {
{i18n.t("ui.sessionReview.title")}
- - options={["unified", "split"]} + 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 088b377cb738..d1ade879e27f 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 9ea722760a2d..89e844732201 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 dab07dab91a9..56c3e083f5c7 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 5ffd2b91155b..0e8b7b9f1047 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 4132125122e0..afba5f648c89 100644 --- a/packages/ui/src/context/dialog.tsx +++ b/packages/ui/src/context/dialog.tsx @@ -28,7 +28,6 @@ const Context = createContext>() function init() { const [active, setActive] = createSignal() - const [renders, setRenders] = createSignal>({}) const timer = { current: undefined as ReturnType | 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 7c8548734d82..3ed0310ef2b6 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 82a913c8830c..8c954f1fe4e7 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); -}