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

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