wip: desktop work

This commit is contained in:
Adam
2025-10-22 17:31:44 -05:00
parent eff12cb484
commit 89b703c387
38 changed files with 1353 additions and 638 deletions

View File

@@ -1,5 +1,4 @@
[data-component="button"] {
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
@@ -32,12 +31,7 @@
border-color: var(--border-weak-base);
background-color: var(--button-secondary-base);
color: var(--text-strong);
/* shadow-xs */
box-shadow:
0 1px 2px -1px rgba(19, 16, 16, 0.04),
0 1px 2px 0 rgba(19, 16, 16, 0.06),
0 1px 3px 0 rgba(19, 16, 16, 0.08);
box-shadow: var(--shadow-xs);
&:hover:not(:disabled) {
border-color: var(--border-hover);
@@ -84,12 +78,11 @@
padding: 0 8px 0 6px;
gap: 8px;
/* 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% */
line-height: var(--line-height-large); /* 171.429% */
letter-spacing: var(--letter-spacing-normal);
}

View File

@@ -1,12 +1,14 @@
import { Button as Kobalte } from "@kobalte/core/button"
import { type ComponentProps, splitProps } from "solid-js"
export interface ButtonProps {
export interface ButtonProps
extends ComponentProps<typeof Kobalte>,
Pick<ComponentProps<"button">, "class" | "classList" | "children"> {
size?: "normal" | "large"
variant?: "primary" | "secondary" | "ghost"
}
export function Button(props: ComponentProps<"button"> & ButtonProps) {
export function Button(props: ButtonProps) {
const [split, rest] = splitProps(props, ["variant", "size", "class", "classList"])
return (
<Kobalte

View File

@@ -0,0 +1,129 @@
/* [data-component="dialog-trigger"] { } */
[data-component="dialog-overlay"] {
position: fixed;
inset: 0;
z-index: 50;
background-color: transparent;
/* animation: overlayHide 250ms ease 100ms forwards; */
/**/
/* &[data-expanded] { */
/* animation: overlayShow 250ms ease; */
/* } */
}
[data-component="dialog"] {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
[data-slot="container"] {
position: relative;
z-index: 50;
width: min(calc(100vw - 16px), 624px);
height: min(calc(100vh - 16px), 512px);
display: flex;
flex-direction: column;
align-items: center;
justify-items: start;
[data-slot="content"] {
display: flex;
flex-direction: column;
align-items: flex-start;
align-self: stretch;
gap: 8px;
width: 100%;
max-height: 100%;
/* padding: 8px; */
padding: 8px 8px 0 8px;
border: 1px solid var(--border-base);
border-radius: 16px;
background: var(--surface-raised-stronger-non-alpha);
box-shadow:
0 15px 45px 0 rgba(19, 16, 16, 0.22),
0 3.35px 10.051px 0 rgba(19, 16, 16, 0.13),
0 0.998px 2.993px 0 rgba(19, 16, 16, 0.09);
/* animation: contentHide 300ms ease-in forwards; */
/**/
/* &[data-expanded] { */
/* animation: contentShow 300ms ease-out; */
/* } */
[data-slot="header"] {
display: flex;
height: 40px;
padding: 4px 4px 4px 8px;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
align-self: stretch;
[data-slot="title"] {
color: var(--text-strong);
/* text-16-medium */
font-family: var(--font-family-sans);
font-size: var(--font-size-large);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-x-large); /* 150% */
letter-spacing: var(--letter-spacing-tight);
}
/* [data-slot="close-button"] {} */
}
/* [data-slot="description"] {} */
[data-slot="body"] {
width: 100%;
position: relative;
display: flex;
flex-direction: column;
flex: 1;
overflow-y: auto;
}
}
}
}
@keyframes overlayShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes overlayHide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes contentShow {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes contentHide {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.96);
}
}

View File

@@ -0,0 +1,91 @@
import {
Dialog as Kobalte,
DialogRootProps,
DialogTitleProps,
DialogCloseButtonProps,
DialogDescriptionProps,
} from "@kobalte/core/dialog"
import { ComponentProps, type JSX, onCleanup, Show, splitProps } from "solid-js"
import { IconButton } from "./icon-button"
export interface DialogProps extends DialogRootProps {
trigger?: JSX.Element
class?: ComponentProps<"div">["class"]
classList?: ComponentProps<"div">["classList"]
}
export function DialogRoot(props: DialogProps) {
let trigger!: HTMLElement
const [local, others] = splitProps(props, ["trigger", "class", "classList", "children"])
const resetTabIndex = () => {
trigger.tabIndex = 0
}
const handleTriggerFocus = (e: FocusEvent & { currentTarget: HTMLElement | null }) => {
const firstChild = e.currentTarget?.firstElementChild as HTMLElement
if (!firstChild) return
firstChild.focus()
trigger.tabIndex = -1
firstChild.addEventListener("focusout", resetTabIndex)
onCleanup(() => {
firstChild.removeEventListener("focusout", resetTabIndex)
})
}
return (
<Kobalte {...others}>
<Show when={props.trigger}>
<Kobalte.Trigger ref={trigger} data-component="dialog-trigger" onFocusIn={handleTriggerFocus}>
{props.trigger}
</Kobalte.Trigger>
</Show>
<Kobalte.Portal>
<Kobalte.Overlay data-component="dialog-overlay" />
<div data-component="dialog">
<div data-slot="container">
<Kobalte.Content
data-slot="content"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.Content>
</div>
</div>
</Kobalte.Portal>
</Kobalte>
)
}
function DialogHeader(props: ComponentProps<"div">) {
return <div data-slot="header" {...props} />
}
function DialogBody(props: ComponentProps<"div">) {
return <div data-slot="body" {...props} />
}
function DialogTitle(props: DialogTitleProps & ComponentProps<"h2">) {
return <Kobalte.Title data-slot="title" {...props} />
}
function DialogDescription(props: DialogDescriptionProps & ComponentProps<"p">) {
return <Kobalte.Description data-slot="description" {...props} />
}
function DialogCloseButton(props: DialogCloseButtonProps & ComponentProps<"button">) {
return <Kobalte.CloseButton data-slot="close-button" as={IconButton} icon="close" {...props} />
}
export const Dialog = Object.assign(DialogRoot, {
Header: DialogHeader,
Title: DialogTitle,
Description: DialogDescription,
CloseButton: DialogCloseButton,
Body: DialogBody,
})

View File

@@ -0,0 +1,117 @@
[data-component="icon-button"] {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 100%;
text-decoration: none;
user-select: none;
aspect-ratio: 1;
&:disabled {
background-color: var(--icon-strong-disabled);
color: var(--icon-invert-base);
cursor: not-allowed;
}
&:focus {
outline: none;
}
&[data-variant="primary"] {
background-color: var(--icon-strong-base);
[data-slot="icon"] {
/* color: var(--icon-weak-base); */
color: var(--icon-invert-base);
/* &:hover:not(:disabled) { */
/* color: var(--icon-weak-hover); */
/* } */
/* &:active:not(:disabled) { */
/* color: var(--icon-string-active); */
/* } */
}
&:hover:not(:disabled) {
background-color: var(--icon-strong-hover);
}
&:active:not(:disabled) {
background-color: var(--icon-string-active);
}
&:focus:not(:disabled) {
background-color: var(--icon-strong-focus);
}
&:disabled {
background-color: var(--icon-strong-disabled);
[data-slot="icon"] {
color: var(--icon-invert-base);
}
}
}
&[data-variant="secondary"] {
background-color: var(--button-secondary-base);
color: var(--text-strong);
&:hover:not(:disabled) {
background-color: var(--surface-hover);
}
&:active:not(:disabled) {
background-color: var(--surface-active);
}
&:focus:not(:disabled) {
background-color: var(--surface-focus);
}
}
&[data-variant="ghost"] {
background-color: transparent;
[data-slot="icon"] {
color: var(--icon-weak-base);
&:hover:not(:disabled) {
color: var(--icon-weak-hover);
}
&:active:not(:disabled) {
color: var(--icon-string-active);
}
}
/* color: var(--text-strong); */
/**/
/* &:hover:not(:disabled) { */
/* background-color: var(--surface-hover); */
/* } */
/* &:active:not(:disabled) { */
/* background-color: var(--surface-active); */
/* } */
/* &:focus:not(:disabled) { */
/* background-color: var(--surface-focus); */
/* } */
}
&[data-size="normal"] {
width: 24px;
height: 24px;
font-size: var(--font-size-small);
line-height: var(--line-height-large);
gap: calc(var(--spacing) * 0.5);
}
&[data-size="large"] {
height: 32px;
padding: 0 8px 0 6px;
gap: 8px;
/* text-12-medium */
font-family: var(--font-family-sans);
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);
}
}

View File

@@ -0,0 +1,27 @@
import { Button as Kobalte } from "@kobalte/core/button"
import { type ComponentProps, splitProps } from "solid-js"
import { Icon, IconProps } from "./icon"
export interface IconButtonProps {
icon: IconProps["name"]
size?: "normal" | "large"
variant?: "primary" | "secondary" | "ghost"
}
export function IconButton(props: ComponentProps<"button"> & IconButtonProps) {
const [split, rest] = splitProps(props, ["variant", "size", "class", "classList"])
return (
<Kobalte
{...rest}
data-component="icon-button"
data-size={split.size || "normal"}
data-variant={split.variant || "secondary"}
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
>
<Icon data-slot="icon" name={props.icon} size={split.size === "large" ? "normal" : "small"} />
</Kobalte>
)
}

View File

@@ -3,4 +3,27 @@
align-items: center;
justify-content: center;
flex-shrink: 0;
/* resize: both; */
aspect-ratio: 1/1;
color: var(--icon-base);
&[data-size="small"] {
width: 16px;
height: 16px;
}
&[data-size="normal"] {
width: 20px;
height: 20px;
}
&[data-size="large"] {
width: 32px;
height: 32px;
}
[data-slot="svg"] {
width: 100%;
height: auto;
}
}

View File

@@ -128,28 +128,55 @@ const icons = {
mic: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.75 8C8.75 6.20507 10.2051 4.75 12 4.75C13.7949 4.75 15.25 6.20507 15.25 8V11C15.25 12.7949 13.7949 14.25 12 14.25C10.2051 14.25 8.75 12.7949 8.75 11V8Z"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5.75 12.75C5.75 12.75 6 17.25 12 17.25C18 17.25 18.25 12.75 18.25 12.75"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 17.75V19.25"></path>',
} as const
const newIcons = {
"circle-x": `<path fill-rule="evenodd" clip-rule="evenodd" d="M1.6665 10.0003C1.6665 5.39795 5.39746 1.66699 9.99984 1.66699C14.6022 1.66699 18.3332 5.39795 18.3332 10.0003C18.3332 14.6027 14.6022 18.3337 9.99984 18.3337C5.39746 18.3337 1.6665 14.6027 1.6665 10.0003ZM7.49984 6.91107L6.91058 7.50033L9.41058 10.0003L6.91058 12.5003L7.49984 13.0896L9.99984 10.5896L12.4998 13.0896L13.0891 12.5003L10.5891 10.0003L13.0891 7.50033L12.4998 6.91107L9.99984 9.41107L7.49984 6.91107Z" fill="currentColor"/>`,
"magnifying-glass": `<path d="M15.8332 15.8337L13.0819 13.0824M14.6143 9.39088C14.6143 12.2759 12.2755 14.6148 9.39039 14.6148C6.50532 14.6148 4.1665 12.2759 4.1665 9.39088C4.1665 6.5058 6.50532 4.16699 9.39039 4.16699C12.2755 4.16699 14.6143 6.5058 14.6143 9.39088Z" stroke="currentColor" stroke-linecap="square"/>`,
"plus-small": `<path d="M9.99984 5.41699V10.0003M9.99984 10.0003V14.5837M9.99984 10.0003H5.4165M9.99984 10.0003H14.5832" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-down": `<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="currentColor" stroke-linecap="square"/>`,
"arrow-up": `<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99991 2.24121L16.0921 8.33343L15.2083 9.21731L10.6249 4.63397V17.5001H9.37492V4.63398L4.7916 9.21731L3.90771 8.33343L9.99991 2.24121Z" fill="currentColor"/>`,
}
export interface IconProps extends ComponentProps<"svg"> {
name: keyof typeof icons
size?: number
name: keyof typeof icons | keyof typeof newIcons
size?: "small" | "normal" | "large"
}
export function Icon(props: IconProps) {
const [local, others] = splitProps(props, ["name", "size", "class", "classList"])
const size = local.size ?? 24
if (local.name in newIcons) {
return (
<div data-component="icon" data-size={local.size || "normal"}>
<svg
data-slot="svg"
classList={{
...(local.classList || {}),
[local.class ?? ""]: !!local.class,
}}
fill="none"
viewBox="0 0 20 20"
innerHTML={newIcons[local.name as keyof typeof newIcons]}
aria-hidden="true"
{...others}
/>
</div>
)
}
return (
<svg
data-component="icon"
classList={{
...(local.classList || {}),
[local.class ?? ""]: !!local.class,
}}
width={size}
height={size}
fill="none"
viewBox="0 0 24 24"
innerHTML={icons[local.name]}
aria-hidden="true"
{...others}
/>
<div data-component="icon" data-size={local.size || "normal"}>
<svg
data-slot="svg"
classList={{
...(local.classList || {}),
[local.class ?? ""]: !!local.class,
}}
fill="none"
viewBox="0 0 24 24"
innerHTML={icons[local.name as keyof typeof icons]}
aria-hidden="true"
{...others}
/>
</div>
)
}

View File

@@ -1,7 +1,11 @@
export * from "./button"
export * from "./dialog"
export * from "./icon"
export * from "./icon-button"
export * from "./input"
export * from "./fonts"
export * from "./list"
export * from "./select"
export * from "./select-dialog"
export * from "./tabs"
export * from "./tooltip"

View File

@@ -0,0 +1,23 @@
[data-component="input"] {
/* [data-slot="label"] {} */
[data-slot="input"] {
color: var(--text-strong);
/* text-14-regular */
font-family: var(--font-family-sans);
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large); /* 142.857% */
letter-spacing: var(--letter-spacing-normal);
&:focus {
outline: none;
}
&::placeholder {
color: var(--text-weak);
}
}
}

View File

@@ -0,0 +1,27 @@
import { TextField as Kobalte } from "@kobalte/core/text-field"
import { Show, splitProps } from "solid-js"
import type { ComponentProps } from "solid-js"
export interface InputProps extends ComponentProps<typeof Kobalte> {
label?: string
hideLabel?: boolean
description?: string
}
export function Input(props: InputProps) {
const [local, others] = splitProps(props, ["class", "label", "hideLabel", "description", "placeholder"])
return (
<Kobalte {...others} data-component="input">
<Show when={local.label}>
<Kobalte.Label data-slot="label" classList={{ "sr-only": local.hideLabel }}>
{local.label}
</Kobalte.Label>
</Show>
<Kobalte.Input data-slot="input" class={local.class} placeholder={local.placeholder} />
<Show when={local.description}>
<Kobalte.Description data-slot="description">{local.description}</Kobalte.Description>
</Show>
<Kobalte.ErrorMessage data-slot="error" />
</Kobalte>
)
}

View File

@@ -12,7 +12,6 @@
scrollbar-width: none;
[data-slot="item"] {
cursor: pointer;
width: 100%;
padding: 4px 12px;
text-align: left;
@@ -23,6 +22,9 @@
&[data-active="true"] {
background-color: var(--surface-raised-base-hover);
}
&:hover {
background-color: var(--surface-raised-base-hover);
}
&:focus {
outline: none;
}

View File

@@ -29,6 +29,7 @@ export function List<T>(props: ListProps<T>) {
// }
const handleSelect = (item: T) => {
props.onSelect?.(item)
list.setActive(props.key(item))
}
const handleKey = (e: KeyboardEvent) => {
@@ -64,10 +65,10 @@ export function List<T>(props: ListProps<T>) {
data-key={props.key(item)}
data-active={props.key(item) === list.active()}
onClick={() => handleSelect(item)}
onMouseMove={(e) => {
e.currentTarget.focus()
onMouseMove={() => {
// e.currentTarget.focus()
setStore("mouseActive", true)
list.setActive(props.key(item))
// list.setActive(props.key(item))
}}
>
{props.children(item)}

View File

@@ -0,0 +1,109 @@
[data-component="select-dialog-input"] {
display: flex;
height: 40px;
flex-shrink: 0;
padding: 4px 10px 4px 6px;
align-items: center;
gap: 12px;
align-self: stretch;
border-radius: 8px;
background: var(--surface-base);
[data-slot="input-container"] {
display: flex;
align-items: center;
gap: 12px;
flex: 1 0 0;
/* [data-slot="icon"] {} */
[data-slot="input"] {
width: 100%;
}
}
/* [data-slot="clear-button"] {} */
}
[data-component="select-dialog"] {
display: flex;
flex-direction: column;
gap: 8px;
[data-slot="empty-state"] {
display: flex;
padding: 32px 160px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
align-self: stretch;
[data-slot="message"] {
display: flex;
justify-content: center;
align-items: center;
gap: 2px;
color: var(--text-weak);
text-align: center;
/* text-14-regular */
font-family: var(--font-family-sans);
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large); /* 142.857% */
letter-spacing: var(--letter-spacing-normal);
}
[data-slot="filter"] {
color: var(--text-strong);
}
}
[data-slot="group"] {
display: flex;
flex-direction: column;
gap: 4px;
[data-slot="header"] {
display: flex;
padding: 4px 8px;
justify-content: space-between;
align-items: center;
align-self: stretch;
color: var(--text-weak);
/* text-12-medium */
font-family: var(--font-family-sans);
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-slot="list"] {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
align-self: stretch;
[data-slot="item"] {
display: flex;
width: 100%;
height: 32px;
padding: 4px 8px 4px 4px;
align-items: center;
&[data-active="true"] {
border-radius: 8px;
background: var(--surface-raised-base-hover);
}
}
}
}
}

View File

@@ -0,0 +1,156 @@
import { createEffect, Show, For, type JSX, splitProps } from "solid-js"
import { Dialog, DialogProps, Icon, IconButton, Input } from "@opencode-ai/ui"
import { createStore } from "solid-js/store"
import { FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
interface SelectDialogProps<T>
extends FilteredListProps<T>,
Pick<DialogProps, "trigger" | "onOpenChange" | "defaultOpen"> {
title: string
placeholder?: string
emptyMessage?: string
children: (item: T) => JSX.Element
onSelect?: (value: T | undefined) => void
}
export function SelectDialog<T>(props: SelectDialogProps<T>) {
const [dialog, others] = splitProps(props, ["trigger", "onOpenChange", "defaultOpen"])
let closeButton!: HTMLButtonElement
let scrollRef: HTMLDivElement | undefined
const [store, setStore] = createStore({
mouseActive: false,
})
const { filter, grouped, flat, reset, clear, active, setActive, onKeyDown, onInput } = useFilteredList<T>({
items: others.items,
key: others.key,
filterKeys: others.filterKeys,
current: others.current,
groupBy: others.groupBy,
sortBy: others.sortBy,
sortGroupsBy: others.sortGroupsBy,
})
createEffect(() => {
filter()
scrollRef?.scrollTo(0, 0)
reset()
})
createEffect(() => {
const all = flat()
if (store.mouseActive || all.length === 0) return
if (active() === others.key(all[0])) {
scrollRef?.scrollTo(0, 0)
return
}
const element = scrollRef?.querySelector(`[data-key="${active()}"]`)
element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
})
const handleInput = (value: string) => {
onInput(value)
reset()
}
const handleSelect = (item: T | undefined) => {
others.onSelect?.(item)
closeButton.click()
}
const handleKey = (e: KeyboardEvent) => {
setStore("mouseActive", false)
if (e.key === "Escape") return
if (e.key === "Enter") {
e.preventDefault()
const selected = flat().find((x) => others.key(x) === active())
if (selected) handleSelect(selected)
} else {
onKeyDown(e)
}
}
const handleOpenChange = (open: boolean) => {
if (!open) clear()
props.onOpenChange?.(open)
}
return (
<Dialog modal {...dialog} onOpenChange={handleOpenChange}>
<Dialog.Header>
<Dialog.Title>{others.title}</Dialog.Title>
<Dialog.CloseButton ref={closeButton} style={{ display: "none" }} />
</Dialog.Header>
<div data-component="select-dialog-input">
<div data-slot="input-container">
<Icon data-slot="icon" name="magnifying-glass" />
<Input
data-slot="input"
type="text"
value={filter()}
onChange={(value) => handleInput(value)}
onKeyDown={handleKey}
placeholder={others.placeholder}
autofocus
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
/>
</div>
<Show when={filter()}>
<IconButton
data-slot="clear-button"
icon="circle-x"
variant="ghost"
onClick={() => {
onInput("")
reset()
}}
/>
</Show>
</div>
<Dialog.Body ref={scrollRef} data-component="select-dialog" class="no-scrollbar">
<Show
when={flat().length > 0}
fallback={
<div data-slot="empty-state">
<div data-slot="message">
{props.emptyMessage ?? "No search results"} for <span data-slot="filter">&quot;{filter()}&quot;</span>
</div>
</div>
}
>
<For each={grouped()}>
{(group) => (
<div data-slot="group">
<Show when={group.category}>
<div data-slot="header">{group.category}</div>
</Show>
<div data-slot="list">
<For each={group.items}>
{(item) => (
<button
data-slot="item"
data-key={others.key(item)}
data-active={others.key(item) === active()}
onClick={() => handleSelect(item)}
onMouseMove={() => {
setStore("mouseActive", true)
setActive(others.key(item))
}}
>
{others.children(item)}
</button>
)}
</For>
</div>
</div>
)}
</For>
</Show>
</Dialog.Body>
</Dialog>
)
}

View File

@@ -1,6 +1,7 @@
[data-component="select"] {
[data-slot="trigger"] {
padding: 0 4px 0 8px;
box-shadow: none;
[data-slot="value"] {
overflow: hidden;
@@ -8,8 +9,8 @@
white-space: nowrap;
}
[data-slot="icon"] {
width: fit-content;
height: fit-content;
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--text-weak);
transition: transform 0.1s ease-in-out;
@@ -18,15 +19,15 @@
}
[data-component="select-content"] {
min-width: 8rem;
min-width: 4rem;
overflow: hidden;
border-radius: var(--radius-md);
border-radius: 8px;
border-width: 1px;
border-style: solid;
border-color: var(--border-weak-base);
background-color: var(--surface-raised-base);
padding: calc(var(--spacing) * 1);
box-shadow: var(--shadow-md);
background-color: var(--surface-raised-stronger-non-alpha);
padding: 2px;
box-shadow: var(--shadow-xs);
z-index: 50;
&[data-closed] {
@@ -42,36 +43,35 @@
max-height: 12rem;
white-space: nowrap;
overflow-x: hidden;
display: flex;
flex-direction: column;
gap: 2px;
&:focus {
outline: none;
}
}
[data-slot="section"] {
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
font-weight: var(--font-weight-light);
text-transform: uppercase;
color: var(--text-weak);
opacity: 0.6;
margin-top: calc(var(--spacing) * 3);
margin-left: calc(var(--spacing) * 2);
&:first-child {
margin-top: 0;
}
}
/* [data-slot="section"] { */
/* } */
[data-slot="item"] {
position: relative;
display: flex;
align-items: center;
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2);
border-radius: var(--radius-sm);
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
color: var(--text-base);
cursor: pointer;
padding: 0 6px 0 6px;
border-radius: 6px;
/* text-12-medium */
font-family: var(--font-family-sans);
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:
background-color 0.2s ease-in-out,
color 0.2s ease-in-out;
@@ -79,24 +79,20 @@
user-select: none;
&[data-highlighted] {
background-color: var(--surface-base);
background: var(--surface-raised-base-hover);
}
&[data-disabled] {
background-color: var(--surface-disabled);
background-color: var(--surface-raised-base);
pointer-events: none;
}
[data-slot="item-indicator"] {
margin-left: auto;
}
&:focus {
outline: none;
}
&:hover {
background-color: var(--surface-hover);
background: var(--surface-raised-base-hover);
}
}
}

View File

@@ -52,7 +52,7 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
{props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)}
</Kobalte.ItemLabel>
<Kobalte.ItemIndicator data-slot="item-indicator">
<Icon name="checkmark" size={16} />
<Icon name="checkmark" />
</Kobalte.ItemIndicator>
</Kobalte.Item>
)}
@@ -79,7 +79,7 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
}}
</Kobalte.Value>
<Kobalte.Icon data-slot="icon">
<Icon name="chevron-down" size={16} />
<Icon name="chevron-down" size="small" />
</Kobalte.Icon>
</Kobalte.Trigger>
<Kobalte.Portal>

View File

@@ -10,7 +10,7 @@
background-color: var(--background-stronger);
overflow: clip;
& [data-slot="list"] {
[data-slot="list"] {
width: 100%;
position: relative;
display: flex;
@@ -40,7 +40,7 @@
}
}
& [data-slot="trigger"] {
[data-slot="trigger"] {
position: relative;
height: 36px;
padding: 8px 12px;
@@ -49,7 +49,7 @@
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
color: var(--text-weak);
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
border-bottom: 1px solid var(--border-weak-base);
@@ -77,7 +77,7 @@
}
}
& [data-slot="content"] {
[data-slot="content"] {
overflow-y: auto;
flex: 1;

View File

@@ -36,7 +36,7 @@ export function Tooltip(props: TooltipProps) {
<KobalteTooltip.Portal>
<KobalteTooltip.Content data-component="tooltip" data-placement={props.placement}>
{typeof others.value === "function" ? others.value() : others.value}
{/* <KobalteTooltip.Arrow data-slot="arrow" size={18} /> */}
{/* <KobalteTooltip.Arrow data-slot="arrow" /> */}
</KobalteTooltip.Content>
</KobalteTooltip.Portal>
</KobalteTooltip>

View File

@@ -0,0 +1 @@
export * from "./use-filtered-list"

View File

@@ -0,0 +1,89 @@
import fuzzysort from "fuzzysort"
import { entries, flatMap, groupBy, map, pipe } from "remeda"
import { createMemo, createResource } from "solid-js"
import { createStore } from "solid-js/store"
import { createList } from "solid-list"
export interface FilteredListProps<T> {
items: T[] | ((filter: string) => Promise<T[]>)
key: (item: T) => string
filterKeys?: string[]
current?: T
groupBy?: (x: T) => string
sortBy?: (a: T, b: T) => number
sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number
onSelect?: (value: T | undefined) => void
}
export function useFilteredList<T>(props: FilteredListProps<T>) {
const [store, setStore] = createStore<{ filter: string }>({ filter: "" })
const [grouped] = createResource(
() => store.filter,
async (filter) => {
const needle = filter?.toLowerCase()
const all = (typeof props.items === "function" ? await props.items(needle) : props.items) || []
const result = pipe(
all,
(x) => {
if (!needle) return x
if (!props.filterKeys && Array.isArray(x) && x.every((e) => typeof e === "string")) {
return fuzzysort.go(needle, x).map((x) => x.target) as T[]
}
return fuzzysort.go(needle, x, { keys: props.filterKeys! }).map((x) => x.obj)
},
groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
entries(),
map(([k, v]) => ({ category: k, items: props.sortBy ? v.sort(props.sortBy) : v })),
(groups) => (props.sortGroupsBy ? groups.sort(props.sortGroupsBy) : groups),
)
return result
},
)
const flat = createMemo(() => {
return pipe(
grouped() || [],
flatMap((x) => x.items),
)
})
const list = createList({
items: () => flat().map(props.key),
initialActive: props.current ? props.key(props.current) : props.key(flat()[0]),
loop: true,
})
const reset = () => {
const all = flat()
if (all.length === 0) return
list.setActive(props.key(all[0]))
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault()
const selected = flat().find((x) => props.key(x) === list.active())
if (selected) props.onSelect?.(selected)
} else {
list.onKeyDown(event)
}
}
const onInput = (value: string) => {
setStore("filter", value)
reset()
}
return {
filter: () => store.filter,
grouped,
flat,
reset,
clear: () => setStore("filter", ""),
onKeyDown,
onInput,
active: list.active,
setActive: list.setActive,
}
}

View File

@@ -6,9 +6,13 @@
@import "./base.css" layer(base);
@import "../components/button.css" layer(components);
@import "../components/dialog.css" layer(components);
@import "../components/icon.css" layer(components);
@import "../components/icon-button.css" layer(components);
@import "../components/input.css" layer(components);
@import "../components/list.css" layer(components);
@import "../components/select.css" layer(components);
@import "../components/select-dialog.css" layer(components);
@import "../components/tabs.css" layer(components);
@import "../components/tooltip.css" layer(components);

View File

@@ -5,11 +5,11 @@
pointer-events: none;
}
::selection {
background-color: color-mix(in srgb, var(--color-primary) 33%, transparent);
/* background-color: var(--color-primary); */
/* color: var(--color-background); */
}
/* ::selection { */
/* background-color: color-mix(in srgb, var(--color-primary) 33%, transparent); */
/* background-color: var(--color-primary); */
/* color: var(--color-background); */
/* } */
::-webkit-scrollbar-track {
background: var(--theme-background-panel);
@@ -36,6 +36,18 @@
}
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.text-12-regular {
font-family: var(--font-family-sans);
font-size: var(--font-size-small);