feat(ui): Select, dropdown, popover styles & transitions (#11675)
This commit is contained in:
@@ -9,7 +9,13 @@
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
outline: none;
|
||||
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;
|
||||
|
||||
&[data-variant="primary"] {
|
||||
background-color: var(--button-primary-base);
|
||||
@@ -94,7 +100,6 @@
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--button-secondary-base);
|
||||
scale: 0.99;
|
||||
transition: all 150ms ease-out;
|
||||
}
|
||||
&:disabled {
|
||||
border-color: var(--border-disabled);
|
||||
@@ -109,34 +114,31 @@
|
||||
}
|
||||
|
||||
&[data-size="small"] {
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
padding: 4px 8px;
|
||||
&[data-icon] {
|
||||
padding: 0 12px 0 4px;
|
||||
padding: 4px 12px 4px 4px;
|
||||
}
|
||||
|
||||
font-size: var(--font-size-small);
|
||||
line-height: var(--line-height-large);
|
||||
gap: 4px;
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
&[data-size="normal"] {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
padding: 0 6px;
|
||||
padding: 4px 6px;
|
||||
&[data-icon] {
|
||||
padding: 0 12px 0 4px;
|
||||
padding: 4px 12px 4px 4px;
|
||||
}
|
||||
|
||||
&[aria-haspopup] {
|
||||
padding: 4px 6px 4px 8px;
|
||||
}
|
||||
|
||||
font-size: var(--font-size-small);
|
||||
gap: 6px;
|
||||
|
||||
/* text-12-medium */
|
||||
@@ -148,7 +150,6 @@
|
||||
}
|
||||
|
||||
&[data-size="large"] {
|
||||
height: 32px;
|
||||
padding: 6px 12px;
|
||||
|
||||
&[data-icon] {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Icon, IconProps } from "./icon"
|
||||
|
||||
export interface ButtonProps
|
||||
extends ComponentProps<typeof Kobalte>,
|
||||
Pick<ComponentProps<"button">, "class" | "classList" | "children"> {
|
||||
Pick<ComponentProps<"button">, "class" | "classList" | "children" | "style"> {
|
||||
size?: "small" | "normal" | "large"
|
||||
variant?: "primary" | "secondary" | "ghost"
|
||||
icon?: IconProps["name"]
|
||||
|
||||
49
packages/ui/src/components/cycle-label.css
Normal file
49
packages/ui/src/components/cycle-label.css
Normal file
@@ -0,0 +1,49 @@
|
||||
.cycle-label {
|
||||
--c-duration: 200ms;
|
||||
--c-stagger: 30ms;
|
||||
--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 var(--transition-duration) var(--transition-easing);
|
||||
will-change: width;
|
||||
overflow: hidden;
|
||||
|
||||
.cycle-char {
|
||||
display: inline-block;
|
||||
transform-style: preserve-3d;
|
||||
min-width: 0.25em;
|
||||
backface-visibility: hidden;
|
||||
|
||||
transition-property: transform, opacity, filter;
|
||||
transition-duration: var(--transition-duration);
|
||||
transition-timing-function: var(--transition-easing);
|
||||
transition-delay: calc(var(--i, 0) * var(--c-stagger));
|
||||
|
||||
&.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
132
packages/ui/src/components/cycle-label.tsx
Normal file
132
packages/ui/src/components/cycle-label.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
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 ?? Number(getComputedStyle(document.documentElement).getPropertyValue("--transition-duration")) ?? 200
|
||||
return typeof d === "function" ? d(text) : d
|
||||
}
|
||||
const stagger = () => props?.stagger ?? 30
|
||||
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-duration": `${getDuration(currentText())}ms`,
|
||||
"--c-stagger": `${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 : {}),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -2,26 +2,29 @@
|
||||
[data-component="dropdown-menu-sub-content"] {
|
||||
min-width: 8rem;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
background-clip: padding-box;
|
||||
background-color: var(--surface-raised-stronger-non-alpha);
|
||||
padding: 4px;
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 50;
|
||||
z-index: 100;
|
||||
transform-origin: var(--kb-menu-content-transform-origin);
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
&:focus-within,
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[data-closed] {
|
||||
animation: dropdown-menu-close 0.15s ease-out;
|
||||
animation: dropdownMenuContentHide var(--transition-duration) var(--transition-easing) forwards;
|
||||
|
||||
@starting-style {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
animation: dropdown-menu-open 0.15s ease-out;
|
||||
pointer-events: auto;
|
||||
animation: dropdownMenuContentShow var(--transition-duration) var(--transition-easing) forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,18 +41,22 @@
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-strong);
|
||||
|
||||
&[data-highlighted] {
|
||||
background: var(--surface-raised-base-hover);
|
||||
transition-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-disabled] {
|
||||
@@ -61,6 +68,8 @@
|
||||
[data-slot="dropdown-menu-sub-trigger"] {
|
||||
&[data-expanded] {
|
||||
background: var(--surface-raised-base-hover);
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,24 +111,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdown-menu-open {
|
||||
@keyframes dropdownMenuContentShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
transform: scaleY(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdown-menu-close {
|
||||
@keyframes dropdownMenuContentHide {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transform: scaleY(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
transform: scaleY(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,13 +80,13 @@ const icons = {
|
||||
|
||||
export interface IconProps extends ComponentProps<"svg"> {
|
||||
name: keyof typeof icons
|
||||
size?: "small" | "normal" | "medium" | "large"
|
||||
size?: "small" | "normal" | "medium" | "large" | number
|
||||
}
|
||||
|
||||
export function Icon(props: IconProps) {
|
||||
const [local, others] = splitProps(props, ["name", "size", "class", "classList"])
|
||||
return (
|
||||
<div data-component="icon" data-size={local.size || "normal"}>
|
||||
<div data-component="icon" data-size={typeof local.size !== 'number' ? local.size || "normal" : `size-[${local.size}px]`}>
|
||||
<svg
|
||||
data-slot="icon-svg"
|
||||
classList={{
|
||||
|
||||
@@ -42,13 +42,13 @@ import { Checkbox } from "./checkbox"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { Markdown } from "./markdown"
|
||||
import { ImagePreview } from "./image-preview"
|
||||
import { findLast } from "@opencode-ai/util/array"
|
||||
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
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: {
|
||||
@@ -415,7 +415,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
||||
toggleExpanded()
|
||||
}}
|
||||
>
|
||||
<Icon name="chevron-down" size="small" />
|
||||
<MorphChevron expanded={expanded()} />
|
||||
</button>
|
||||
<div data-slot="user-message-copy-wrapper">
|
||||
<Tooltip
|
||||
@@ -898,7 +898,7 @@ ToolRegistry.register({
|
||||
if (!sessionId) return undefined
|
||||
// Find the tool part that matches the permission's callID
|
||||
const messages = data.store.message[sessionId] ?? []
|
||||
const message = findLast(messages, (m) => m.id === perm.tool!.messageID)
|
||||
const message = messages.findLast((m) => m.id === perm.tool!.messageID)
|
||||
if (!message) return undefined
|
||||
const parts = data.store.part[message.id] ?? []
|
||||
for (const part of parts) {
|
||||
|
||||
10
packages/ui/src/components/morph-chevron.css
Normal file
10
packages/ui/src/components/morph-chevron.css
Normal file
@@ -0,0 +1,10 @@
|
||||
[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;
|
||||
}
|
||||
73
packages/ui/src/components/morph-chevron.tsx
Normal file
73
packages/ui/src/components/morph-chevron.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -15,16 +15,35 @@
|
||||
|
||||
transform-origin: var(--kb-popover-content-transform-origin);
|
||||
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
animation: popoverContentHide var(--transition-duration) var(--transition-easing) forwards;
|
||||
|
||||
&[data-closed] {
|
||||
animation: popover-close 0.15s ease-out;
|
||||
@starting-style {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
animation: popover-open 0.15s ease-out;
|
||||
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;
|
||||
}
|
||||
|
||||
[data-slot="popover-header"] {
|
||||
@@ -75,24 +94,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes popover-open {
|
||||
@keyframes popoverContentShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
transform: scaleY(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes popover-close {
|
||||
@keyframes popoverContentHide {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transform: scaleY(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
9
packages/ui/src/components/reasoning-icon.css
Normal file
9
packages/ui/src/components/reasoning-icon.css
Normal file
@@ -0,0 +1,9 @@
|
||||
[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);
|
||||
opacity: calc(var(--reasoning-icon-percentage) * 0.75);
|
||||
}
|
||||
}
|
||||
32
packages/ui/src/components/reasoning-icon.tsx
Normal file
32
packages/ui/src/components/reasoning-icon.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
[data-component="select"] {
|
||||
[data-slot="select-select-trigger"] {
|
||||
padding: 0 4px 0 8px;
|
||||
display: flex;
|
||||
padding: 4px 8px !important;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: none;
|
||||
transition-property: background-color;
|
||||
transition-duration: var(--transition-duration);
|
||||
transition-timing-function: var(--transition-easing);
|
||||
|
||||
[data-slot="select-select-trigger-value"] {
|
||||
overflow: hidden;
|
||||
@@ -15,10 +21,10 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-weak);
|
||||
transition: transform 0.1s ease-in-out;
|
||||
color: var(--icon-base);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&[data-expanded] {
|
||||
&[data-variant="secondary"] {
|
||||
background-color: var(--button-secondary-hover);
|
||||
@@ -30,13 +36,13 @@
|
||||
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: var(--surface-raised-base-hover);
|
||||
background-color: transparent;
|
||||
}
|
||||
&[data-variant="primary"] {
|
||||
background-color: var(--icon-strong-base);
|
||||
@@ -46,10 +52,10 @@
|
||||
|
||||
&[data-trigger-style="settings"] {
|
||||
[data-slot="select-select-trigger"] {
|
||||
padding: 6px 6px 6px 12px;
|
||||
padding: 6px 6px 6px 10px;
|
||||
box-shadow: none;
|
||||
border-radius: 6px;
|
||||
min-width: 160px;
|
||||
field-sizing: content;
|
||||
height: 32px;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
@@ -61,6 +67,7 @@
|
||||
white-space: nowrap;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-regular);
|
||||
padding: 4px 8px 4px 4px;
|
||||
}
|
||||
[data-slot="select-select-trigger-icon"] {
|
||||
width: 16px;
|
||||
@@ -91,17 +98,26 @@
|
||||
}
|
||||
|
||||
[data-component="select-content"] {
|
||||
min-width: 104px;
|
||||
min-width: 8rem;
|
||||
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: 60;
|
||||
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;
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
animation: select-open 0.15s ease-out;
|
||||
pointer-events: auto;
|
||||
animation: selectContentShow var(--transition-duration) var(--transition-easing) forwards;
|
||||
}
|
||||
|
||||
[data-slot="select-select-content-list"] {
|
||||
@@ -111,43 +127,38 @@
|
||||
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: 2px 8px;
|
||||
padding: 4px 8px;
|
||||
gap: 12px;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
color: var(--text-strong);
|
||||
|
||||
transition:
|
||||
background-color 0.2s ease-in-out,
|
||||
color 0.2s ease-in-out;
|
||||
transition-property: background-color, color;
|
||||
transition-duration: var(--transition-duration);
|
||||
transition-timing-function: var(--transition-easing);
|
||||
outline: none;
|
||||
user-select: none;
|
||||
|
||||
&[data-highlighted] {
|
||||
background: var(--surface-raised-base-hover);
|
||||
&:hover {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
&[data-disabled] {
|
||||
background-color: var(--surface-raised-base);
|
||||
@@ -160,6 +171,11 @@
|
||||
margin-left: auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--icon-strong-base);
|
||||
|
||||
svg {
|
||||
color: var(--icon-strong-base);
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
@@ -171,13 +187,9 @@
|
||||
}
|
||||
|
||||
[data-component="select-content"][data-trigger-style="settings"] {
|
||||
min-width: 160px;
|
||||
field-sizing: content;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
|
||||
[data-slot="select-select-content-list"] {
|
||||
padding: 4px;
|
||||
}
|
||||
padding: 0 0 0 4px;
|
||||
|
||||
[data-slot="select-select-item"] {
|
||||
/* text-14-regular */
|
||||
@@ -190,13 +202,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes select-open {
|
||||
@keyframes selectContentShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
transform: scaleY(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes selectContentHide {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scaleY(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Select as Kobalte } from "@kobalte/core/select"
|
||||
import { createMemo, onCleanup, splitProps, type ComponentProps, type JSX } from "solid-js"
|
||||
import { createMemo, createSignal, 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
|
||||
@@ -38,6 +40,8 @@ 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,
|
||||
@@ -85,7 +89,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={4}
|
||||
gutter={8}
|
||||
value={local.current}
|
||||
options={grouped()}
|
||||
optionValue={(x) => (local.value ? local.value(x) : (x as string))}
|
||||
@@ -115,7 +119,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-small" size="small" />
|
||||
<Icon name="check" size="small" />
|
||||
</Kobalte.ItemIndicator>
|
||||
</Kobalte.Item>
|
||||
)}
|
||||
@@ -124,6 +128,7 @@ export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">)
|
||||
stop()
|
||||
}}
|
||||
onOpenChange={(open) => {
|
||||
setIsOpen(open)
|
||||
local.onOpenChange?.(open)
|
||||
if (!open) stop()
|
||||
}}
|
||||
@@ -149,7 +154,12 @@ export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">)
|
||||
}}
|
||||
</Kobalte.Value>
|
||||
<Kobalte.Icon data-slot="select-select-trigger-icon">
|
||||
<Icon name={local.triggerVariant === "settings" ? "selector" : "chevron-down"} size="small" />
|
||||
<Show when={local.triggerVariant === "settings"}>
|
||||
<Icon name="selector" size="small" />
|
||||
</Show>
|
||||
<Show when={local.triggerVariant !== "settings"}>
|
||||
<MorphChevron expanded={isOpen()} />
|
||||
</Show>
|
||||
</Kobalte.Icon>
|
||||
</Kobalte.Trigger>
|
||||
<Kobalte.Portal>
|
||||
@@ -166,4 +176,4 @@ export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">)
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user