Revert "feat: Transitions, spacing, scroll fade, prompt area update (#11168)" (#11461)

Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com>
This commit is contained in:
Adam
2026-01-31 07:18:51 -06:00
committed by GitHub
parent 511c7abaca
commit a552652fcc
49 changed files with 382 additions and 1323 deletions

View File

@@ -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)

View File

@@ -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>

View File

@@ -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")}
>

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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();

View File

@@ -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-expanded] {
[data-slot="accordion-trigger"] {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
[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-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-expanded] {
grid-template-rows: 1fr;
}
[data-slot="accordion-content"][data-closed] {
grid-template-rows: 0fr;
}
&[data-expanded] [data-slot="accordion-trigger"] {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
&[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;
[data-slot="accordion-content"] {
overflow: hidden;
width: 100%;
}
}
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--kb-accordion-content-height);
}
}
@keyframes slideUp {
from {
height: var(--kb-accordion-content-height);
}
to {
height: 0;
}
}

View File

@@ -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,
})

View File

@@ -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);
}

View File

@@ -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"]

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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>
)
}

View File

@@ -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));
}
}
}

View File

@@ -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 : {}),
}}
/>
)
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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"] {

View File

@@ -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); */

View File

@@ -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"] {

View File

@@ -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={{

View File

@@ -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);
}

View File

@@ -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>
)
}

View File

@@ -1,4 +1,4 @@
[data-component="logo-mark"] {
width: 16px;
aspect-ratio: 4 / 5;
aspect-ratio: 4/5;
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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>
)
}

View File

@@ -15,35 +15,16 @@
transform-origin: var(--kb-popover-content-transform-origin);
animation: popoverContentHide var(--transition-duration) var(--transition-easing) forwards;
&:focus-within {
outline: none;
}
@starting-style {
animation: none;
&[data-closed] {
animation: popover-close 0.15s ease-out;
}
&[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;
}
[data-origin-bottom-left] {
transform-origin: bottom left;
}
&:focus-within {
outline: none;
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);
}
}

View File

@@ -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);
}
}

View File

@@ -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>
)

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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>
)
}

View File

@@ -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,

View File

@@ -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);
}
}
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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}
>

View File

@@ -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"] {

View File

@@ -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"] {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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()
},

View File

@@ -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);

View File

@@ -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);
}