feat(app): polish select-v2 component (#30446)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
This commit is contained in:
parent
2538c0d083
commit
134a5c818a
@ -2,7 +2,7 @@ import { Component, Show, createMemo, createResource, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { SelectV2 } from "@opencode-ai/ui/select-v2"
|
||||
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
|
||||
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
|
||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
@ -289,7 +289,7 @@ export const SettingsGeneralV2: Component = () => {
|
||||
if (!option) return
|
||||
playDemoSound(option.id === "none" ? undefined : option.id)
|
||||
},
|
||||
onSelect: (option: (typeof soundOptions)[number] | undefined) => {
|
||||
onSelect: (option: (typeof soundOptions)[number] | null) => {
|
||||
if (!option) return
|
||||
if (option.id === "none") {
|
||||
setEnabled(false)
|
||||
@ -310,8 +310,11 @@ export const SettingsGeneralV2: Component = () => {
|
||||
description={language.t("settings.general.row.language.description")}
|
||||
>
|
||||
<SelectV2
|
||||
appearance="inline"
|
||||
data-action="settings-language"
|
||||
options={languageOptions()}
|
||||
placement="bottom-end"
|
||||
gutter={6}
|
||||
current={languageOptions().find((o) => o.value === language.locale())}
|
||||
value={(o) => o.value}
|
||||
label={(o) => o.label}
|
||||
@ -333,9 +336,12 @@ export const SettingsGeneralV2: Component = () => {
|
||||
description={language.t("settings.general.row.shell.description")}
|
||||
>
|
||||
<SelectV2
|
||||
appearance="inline"
|
||||
data-action="settings-shell"
|
||||
options={shellOptions()}
|
||||
current={shellOptions().find((o) => o.value === currentShell()) ?? autoOption}
|
||||
placement="bottom-end"
|
||||
gutter={6}
|
||||
value={(o) => o.id}
|
||||
label={(o) => o.label}
|
||||
onSelect={(option) => {
|
||||
@ -505,9 +511,12 @@ export const SettingsGeneralV2: Component = () => {
|
||||
description={language.t("settings.general.row.colorScheme.description")}
|
||||
>
|
||||
<SelectV2
|
||||
appearance="inline"
|
||||
data-action="settings-color-scheme"
|
||||
options={colorSchemeOptions()}
|
||||
current={colorSchemeOptions().find((o) => o.value === theme.colorScheme())}
|
||||
placement="bottom-end"
|
||||
gutter={6}
|
||||
value={(o) => o.value}
|
||||
label={(o) => o.label}
|
||||
onSelect={(option) => option && theme.setColorScheme(option.value)}
|
||||
@ -531,9 +540,12 @@ export const SettingsGeneralV2: Component = () => {
|
||||
}
|
||||
>
|
||||
<SelectV2
|
||||
appearance="inline"
|
||||
data-action="settings-theme"
|
||||
options={themeOptions()}
|
||||
current={themeOptions().find((o) => o.id === theme.themeId())}
|
||||
placement="bottom-end"
|
||||
gutter={6}
|
||||
value={(o) => o.id}
|
||||
label={(o) => o.name}
|
||||
onSelect={(option) => {
|
||||
@ -671,6 +683,7 @@ export const SettingsGeneralV2: Component = () => {
|
||||
description={language.t("settings.general.sounds.agent.description")}
|
||||
>
|
||||
<SelectV2
|
||||
appearance="inline"
|
||||
data-action="settings-sounds-agent"
|
||||
{...soundSelectProps(
|
||||
() => settings.sounds.agentEnabled(),
|
||||
@ -678,6 +691,8 @@ export const SettingsGeneralV2: Component = () => {
|
||||
(value) => settings.sounds.setAgentEnabled(value),
|
||||
(id) => settings.sounds.setAgent(id),
|
||||
)}
|
||||
placement="bottom-end"
|
||||
gutter={6}
|
||||
/>
|
||||
</SettingsRowV2>
|
||||
|
||||
@ -686,6 +701,7 @@ export const SettingsGeneralV2: Component = () => {
|
||||
description={language.t("settings.general.sounds.permissions.description")}
|
||||
>
|
||||
<SelectV2
|
||||
appearance="inline"
|
||||
data-action="settings-sounds-permissions"
|
||||
{...soundSelectProps(
|
||||
() => settings.sounds.permissionsEnabled(),
|
||||
@ -693,6 +709,8 @@ export const SettingsGeneralV2: Component = () => {
|
||||
(value) => settings.sounds.setPermissionsEnabled(value),
|
||||
(id) => settings.sounds.setPermissions(id),
|
||||
)}
|
||||
placement="bottom-end"
|
||||
gutter={6}
|
||||
/>
|
||||
</SettingsRowV2>
|
||||
|
||||
@ -701,6 +719,7 @@ export const SettingsGeneralV2: Component = () => {
|
||||
description={language.t("settings.general.sounds.errors.description")}
|
||||
>
|
||||
<SelectV2
|
||||
appearance="inline"
|
||||
data-action="settings-sounds-errors"
|
||||
{...soundSelectProps(
|
||||
() => settings.sounds.errorsEnabled(),
|
||||
@ -708,6 +727,8 @@ export const SettingsGeneralV2: Component = () => {
|
||||
(value) => settings.sounds.setErrorsEnabled(value),
|
||||
(id) => settings.sounds.setErrors(id),
|
||||
)}
|
||||
placement="bottom-end"
|
||||
gutter={6}
|
||||
/>
|
||||
</SettingsRowV2>
|
||||
</SettingsListV2>
|
||||
|
||||
@ -153,14 +153,12 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-component="settings-v2-dialog"] [data-component="select"][data-trigger-style="settings-v2"] {
|
||||
[data-component="settings-v2-dialog"] [data-component="select-v2-root"] {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
[data-component="settings-v2-dialog"]
|
||||
[data-component="select"][data-trigger-style="settings-v2"]
|
||||
[data-component="button-v2"] {
|
||||
[data-component="settings-v2-dialog"] [data-component="button-v2"] {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: fit-content;
|
||||
height: 24px;
|
||||
padding: 4px 4px 4px 8px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--v2-text-text-base);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger-value"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
height: 13px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-style: normal;
|
||||
font-weight: 530;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.04px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-variation-settings: "slnt" 0;
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger"]:focus-visible {
|
||||
outline: 2px solid var(--v2-border-border-focus);
|
||||
outline-offset: 2.5px;
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"]
|
||||
[data-slot="select-select-trigger"]:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"]
|
||||
[data-slot="select-select-trigger"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger"]:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger-icon"] {
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"]
|
||||
[data-slot="select-select-trigger-icon"]
|
||||
[data-slot="icon-svg"] {
|
||||
margin-inline: -5px;
|
||||
color: #3a3a3a;
|
||||
}
|
||||
|
||||
[data-component="select-content"][data-trigger-style="settings-v2"] {
|
||||
min-width: 160px;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
|
||||
[data-slot="select-select-content-list"] {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
[data-slot="select-select-item"] {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./select-v2"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const story = create({
|
||||
title: "UI/SelectV2",
|
||||
mod,
|
||||
args: {
|
||||
options: ["One", "Two", "Three"],
|
||||
current: "One",
|
||||
placeholder: "Choose...",
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/SelectV2",
|
||||
id: "components-select-v2",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
@ -1,171 +0,0 @@
|
||||
import { Select as Kobalte } from "@kobalte/core/select"
|
||||
import { createMemo, onCleanup, splitProps, type ComponentProps, type JSX } from "solid-js"
|
||||
import { pipe, groupBy, entries, map } from "remeda"
|
||||
import { Icon as IconV2 } from "../v2/components/icon"
|
||||
import { Icon } from "./icon"
|
||||
import "./select-v2.css"
|
||||
|
||||
export type SelectV2Props<T> = Omit<ComponentProps<typeof Kobalte<T>>, "value" | "onSelect" | "children"> & {
|
||||
placeholder?: string
|
||||
options: T[]
|
||||
current?: T
|
||||
value?: (x: T) => string
|
||||
label?: (x: T) => string
|
||||
groupBy?: (x: T) => string
|
||||
valueClass?: ComponentProps<"div">["class"]
|
||||
onSelect?: (value: T | undefined) => void
|
||||
onHighlight?: (value: T | undefined) => (() => void) | void
|
||||
class?: ComponentProps<"div">["class"]
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
children?: (item: T | undefined) => JSX.Element
|
||||
triggerStyle?: JSX.CSSProperties
|
||||
triggerProps?: Record<string, string | number | boolean | undefined>
|
||||
}
|
||||
|
||||
export function SelectV2<T>(props: SelectV2Props<T> & { disabled?: boolean }) {
|
||||
const [local, others] = splitProps(props, [
|
||||
"class",
|
||||
"classList",
|
||||
"placeholder",
|
||||
"options",
|
||||
"current",
|
||||
"value",
|
||||
"label",
|
||||
"groupBy",
|
||||
"valueClass",
|
||||
"onSelect",
|
||||
"onHighlight",
|
||||
"onOpenChange",
|
||||
"children",
|
||||
"triggerStyle",
|
||||
"triggerProps",
|
||||
])
|
||||
|
||||
const state = {
|
||||
key: undefined as string | undefined,
|
||||
cleanup: undefined as (() => void) | void,
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
state.cleanup?.()
|
||||
state.cleanup = undefined
|
||||
state.key = undefined
|
||||
}
|
||||
|
||||
const keyFor = (item: T) => (local.value ? local.value(item) : (item as string))
|
||||
|
||||
const move = (item: T | undefined) => {
|
||||
if (!local.onHighlight) return
|
||||
if (!item) {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
|
||||
const key = keyFor(item)
|
||||
if (state.key === key) return
|
||||
state.cleanup?.()
|
||||
state.cleanup = local.onHighlight(item)
|
||||
state.key = key
|
||||
}
|
||||
|
||||
onCleanup(stop)
|
||||
|
||||
const grouped = createMemo(() => {
|
||||
const result = pipe(
|
||||
local.options,
|
||||
groupBy((x) => (local.groupBy ? local.groupBy(x) : "")),
|
||||
entries(),
|
||||
map(([k, v]) => ({ category: k, options: v })),
|
||||
)
|
||||
return result
|
||||
})
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Kobalte<T, { category: string; options: T[] }>
|
||||
{...others}
|
||||
data-component="select"
|
||||
data-trigger-style="settings-v2"
|
||||
placement="bottom-end"
|
||||
gutter={4}
|
||||
value={local.current}
|
||||
options={grouped()}
|
||||
optionValue={(x) => (local.value ? local.value(x) : (x as string))}
|
||||
optionTextValue={(x) => (local.label ? local.label(x) : (x as string))}
|
||||
optionGroupChildren="options"
|
||||
placeholder={local.placeholder}
|
||||
sectionComponent={(local) => (
|
||||
<Kobalte.Section data-slot="select-section">{local.section.rawValue.category}</Kobalte.Section>
|
||||
)}
|
||||
itemComponent={(itemProps) => (
|
||||
<Kobalte.Item
|
||||
{...itemProps}
|
||||
data-slot="select-select-item"
|
||||
classList={{
|
||||
...local.classList,
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
onPointerEnter={() => move(itemProps.item.rawValue)}
|
||||
onPointerMove={() => move(itemProps.item.rawValue)}
|
||||
onFocus={() => move(itemProps.item.rawValue)}
|
||||
>
|
||||
<Kobalte.ItemLabel data-slot="select-select-item-label">
|
||||
{local.children
|
||||
? local.children(itemProps.item.rawValue)
|
||||
: local.label
|
||||
? local.label(itemProps.item.rawValue)
|
||||
: (itemProps.item.rawValue as string)}
|
||||
</Kobalte.ItemLabel>
|
||||
<Kobalte.ItemIndicator data-slot="select-select-item-indicator">
|
||||
<Icon name="check-small" size="small" />
|
||||
</Kobalte.ItemIndicator>
|
||||
</Kobalte.Item>
|
||||
)}
|
||||
onChange={(v) => {
|
||||
local.onSelect?.(v ?? undefined)
|
||||
stop()
|
||||
}}
|
||||
onOpenChange={(open) => {
|
||||
local.onOpenChange?.(open)
|
||||
if (!open) stop()
|
||||
}}
|
||||
>
|
||||
<Kobalte.Trigger
|
||||
{...local.triggerProps}
|
||||
type="button"
|
||||
disabled={props.disabled}
|
||||
data-slot="select-select-trigger"
|
||||
as="button"
|
||||
style={local.triggerStyle}
|
||||
classList={{
|
||||
...local.classList,
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
<Kobalte.Value<T> data-slot="select-select-trigger-value" class={local.valueClass}>
|
||||
{(state) => {
|
||||
const selected = state.selectedOption() ?? local.current
|
||||
if (!selected) return local.placeholder || ""
|
||||
if (local.label) return local.label(selected)
|
||||
return selected as string
|
||||
}}
|
||||
</Kobalte.Value>
|
||||
<Kobalte.Icon data-slot="select-select-trigger-icon">
|
||||
<IconV2 name="chevron-down" class="-mx-[5px]" />
|
||||
</Kobalte.Icon>
|
||||
</Kobalte.Trigger>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Content
|
||||
classList={{
|
||||
...local.classList,
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
data-component="select-content"
|
||||
data-trigger-style="settings-v2"
|
||||
>
|
||||
<Kobalte.Listbox data-slot="select-select-content-list" />
|
||||
</Kobalte.Content>
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
@ -1,7 +1,21 @@
|
||||
@import "./menu-v2.css";
|
||||
|
||||
/* Select dropdown: slide down from trigger (no scale-from-corner). */
|
||||
/* Above modal dialogs (z-index 50); matches legacy select-content. */
|
||||
[data-popper-positioner]:has([data-slot="select-v2-content"]) {
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
/* Dropdown surface (Type=menu) — overrides shared menu-v2 defaults for selects only. */
|
||||
[data-component="menu-v2-content"][data-slot="select-v2-content"] {
|
||||
padding: 0;
|
||||
min-width: 160px;
|
||||
max-width: 23rem;
|
||||
overflow: hidden;
|
||||
z-index: 60;
|
||||
pointer-events: auto;
|
||||
background: var(--v2-background-bg-layer-01);
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--v2-elevation-floating);
|
||||
transform-origin: top center;
|
||||
animation: select-v2-content-in 120ms ease-out;
|
||||
}
|
||||
@ -46,8 +60,9 @@
|
||||
border-radius: 6px;
|
||||
outline: 1px solid transparent;
|
||||
outline-offset: 0;
|
||||
background: linear-gradient(180deg, var(--alpha-light-2) 0%, var(--alpha-light-0) 100%), var(--background-bg-base);
|
||||
box-shadow: var(--elevation-button-neutral);
|
||||
background:
|
||||
linear-gradient(180deg, var(--v2-alpha-light-2) 0%, var(--v2-alpha-light-0) 100%), var(--v2-background-bg-base);
|
||||
box-shadow: var(--v2-elevation-button-neutral);
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
transition:
|
||||
@ -65,18 +80,24 @@
|
||||
[data-expanded]
|
||||
) {
|
||||
background:
|
||||
linear-gradient(0deg, var(--overlay-simple-overlay-hover), var(--overlay-simple-overlay-hover)),
|
||||
linear-gradient(180deg, var(--alpha-light-2) 0%, var(--alpha-light-0) 100%), var(--background-bg-base);
|
||||
linear-gradient(0deg, var(--v2-overlay-simple-overlay-hover), var(--v2-overlay-simple-overlay-hover)),
|
||||
linear-gradient(180deg, var(--v2-alpha-light-2) 0%, var(--v2-alpha-light-0) 100%), var(--v2-background-bg-base);
|
||||
}
|
||||
|
||||
[data-component="select-v2"]:where(:focus-within):not([data-disabled], [data-invalid]),
|
||||
[data-component="select-v2"]:where([data-expanded]):not([data-disabled], [data-invalid]) {
|
||||
outline-color: var(--border-border-focus);
|
||||
background:
|
||||
linear-gradient(0deg, var(--v2-overlay-simple-overlay-hover), var(--v2-overlay-simple-overlay-hover)),
|
||||
linear-gradient(180deg, var(--v2-alpha-light-2) 0%, var(--v2-alpha-light-0) 100%), var(--v2-background-bg-base);
|
||||
outline-color: transparent;
|
||||
}
|
||||
|
||||
[data-component="select-v2"]:where(:focus-within):not([data-disabled], [data-invalid]):not([data-expanded]) {
|
||||
outline-color: var(--v2-border-border-focus);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-component="select-v2"]:where([data-invalid]):not([data-disabled]) {
|
||||
outline-color: var(--state-fg-danger);
|
||||
outline-color: var(--v2-state-fg-danger);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@ -115,13 +136,13 @@
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.04px;
|
||||
color: var(--text-text-base);
|
||||
color: var(--v2-text-text-base);
|
||||
font-variation-settings: "slnt" 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
[data-component="select-v2"] [data-slot="select-v2-value-text"][data-placeholder-shown] {
|
||||
color: var(--text-text-faint);
|
||||
color: var(--v2-text-text-faint);
|
||||
}
|
||||
|
||||
[data-component="select-v2"][data-numeric] [data-slot="select-v2-value-text"] {
|
||||
@ -129,7 +150,7 @@
|
||||
}
|
||||
|
||||
[data-component="select-v2"]:where([data-invalid]):not([data-disabled]) [data-slot="select-v2-value-text"] {
|
||||
color: var(--state-fg-danger);
|
||||
color: var(--v2-state-fg-danger);
|
||||
}
|
||||
|
||||
[data-component="select-v2"] [data-slot="select-v2-chevron"] {
|
||||
@ -146,7 +167,7 @@
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--icon-icon-muted);
|
||||
color: var(--v2-icon-icon-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@ -161,6 +182,49 @@
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
/* Compact trigger for settings rows and similar inline contexts. */
|
||||
[data-component="select-v2"][data-appearance="inline"] {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
height: 24px;
|
||||
padding: 4px 4px 4px 8px;
|
||||
gap: 4px;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
align-self: auto;
|
||||
}
|
||||
|
||||
[data-component="select-v2"][data-appearance="inline"]:where(:hover):not([data-disabled], [data-invalid]):not(
|
||||
:focus-within
|
||||
):not([data-expanded]) {
|
||||
background: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-component="select-v2"][data-appearance="inline"]:where([data-expanded]):not([data-disabled], [data-invalid]) {
|
||||
background: var(--v2-overlay-simple-overlay-hover);
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-component="select-v2"][data-appearance="inline"]:where(:active):not([data-disabled], [data-invalid]):not(
|
||||
[data-expanded]
|
||||
) {
|
||||
background: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-component="select-v2"][data-appearance="inline"] [data-slot="select-v2-value-text"] {
|
||||
padding: 0;
|
||||
font-weight: 530;
|
||||
}
|
||||
|
||||
[data-component="select-v2"][data-appearance="inline"] [data-slot="select-v2-chevron"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Listbox inside menu surface */
|
||||
[data-component="menu-v2-content"][data-slot="select-v2-content"] [data-slot="select-v2-listbox"] {
|
||||
box-sizing: border-box;
|
||||
@ -168,13 +232,39 @@
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding: 4px;
|
||||
list-style: none;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-height: min(320px, 70vh);
|
||||
max-height: 12rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
> *:not([role="presentation"]) + *:not([role="presentation"]) {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="select-v2-listbox"] [data-component="menu-v2-item"],
|
||||
[data-slot="select-v2-listbox"] [data-slot="menu-v2-group-label"] {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
[data-slot="select-v2-listbox"] [data-component="menu-v2-item"] {
|
||||
--menu-v2-fg: var(--v2-text-text-base);
|
||||
--menu-v2-fg-muted: var(--v2-text-text-faint);
|
||||
--menu-v2-fg-subtle: var(--v2-text-text-muted);
|
||||
--menu-v2-icon: var(--v2-icon-icon-base);
|
||||
--menu-v2-accent: var(--v2-text-text-accent);
|
||||
--menu-v2-badge-bg: var(--v2-background-bg-layer-02);
|
||||
--menu-v2-badge-border: var(--v2-border-border-base);
|
||||
--menu-v2-hover: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
/* Listbox uses data-selected; menu item CSS uses data-checked — mirror accent */
|
||||
|
||||
@ -22,7 +22,8 @@ Single-select built on Kobalte with a **TextInput v2** trigger surface and **Men
|
||||
- \`options\`, \`current\`, \`onSelect\`: controlled selection (\`current\` is the selected option object).
|
||||
- \`value\` / \`label\`: accessors when options are not plain strings.
|
||||
- \`groupBy\`: groups options; section headers use menu group label styling.
|
||||
- \`appearance\`: \`base\` (28px) or \`large\` (32px).
|
||||
- \`appearance\`: \`base\` (28px), \`large\` (32px), or \`inline\` (compact settings-row trigger).
|
||||
- \`placement\`, \`gutter\`, \`sameWidth\`, \`flip\`, \`slide\`, \`fitViewport\`: forwarded to Kobalte popper (defaults match legacy \`Select\`: gutter 4, flip/slide on; inline uses \`bottom-end\` and \`sameWidth: false\`).
|
||||
- \`invalid\`, \`disabled\`, \`numeric\`: match text input conventions.
|
||||
`
|
||||
|
||||
@ -58,7 +59,7 @@ export default {
|
||||
},
|
||||
appearance: {
|
||||
control: "select",
|
||||
options: ["base", "large"],
|
||||
options: ["base", "large", "inline"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -53,8 +53,8 @@ export type SelectV2Props<T> = Omit<
|
||||
groupBy?: (x: T) => string
|
||||
onSelect?: (value: T | null) => void
|
||||
onHighlight?: (value: T | undefined) => void | (() => void)
|
||||
/** Match TextInput v2 height. */
|
||||
appearance?: "base" | "large"
|
||||
/** `base` / `large` match text-input-v2; `inline` is a compact settings-row trigger. */
|
||||
appearance?: "base" | "large" | "inline"
|
||||
invalid?: boolean
|
||||
numeric?: boolean
|
||||
children?: (item: T) => JSX.Element
|
||||
@ -80,8 +80,16 @@ export function SelectV2<T>(props: SelectV2Props<T>) {
|
||||
"numeric",
|
||||
"disabled",
|
||||
"valueClass",
|
||||
"placement",
|
||||
"gutter",
|
||||
"sameWidth",
|
||||
"flip",
|
||||
"slide",
|
||||
"fitViewport",
|
||||
])
|
||||
|
||||
const inline = () => (local.appearance ?? "base") === "inline"
|
||||
|
||||
const state: { key?: string; cleanup?: void | (() => void) } = {}
|
||||
|
||||
const stop = () => {
|
||||
@ -115,8 +123,12 @@ export function SelectV2<T>(props: SelectV2Props<T>) {
|
||||
multiple={false}
|
||||
disabled={local.disabled}
|
||||
data-component="select-v2-root"
|
||||
gutter={6}
|
||||
placement="bottom-start"
|
||||
placement={local.placement ?? (inline() ? "bottom-end" : "bottom-start")}
|
||||
gutter={local.gutter ?? 4}
|
||||
sameWidth={local.sameWidth ?? !inline()}
|
||||
flip={local.flip ?? true}
|
||||
slide={local.slide ?? true}
|
||||
fitViewport={local.fitViewport ?? false}
|
||||
value={local.current}
|
||||
options={grouped()}
|
||||
optionValue={(x) => (local.value ? local.value(x) : String(x as string))}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user