瀏覽代碼

feat(app): polish select-v2 component (#30446)

Co-authored-by: Brendan Allan <git@brendonovich.dev>
Aarav Sareen 2 月之前
父節點
當前提交
134a5c818a

+ 23 - 2
packages/app/src/components/settings-v2/general.tsx

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

+ 2 - 4
packages/app/src/components/settings-v2/settings-v2.css

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

+ 0 - 87
packages/ui/src/components/select-v2.css

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

+ 0 - 22
packages/ui/src/components/select-v2.stories.tsx

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

+ 0 - 171
packages/ui/src/components/select-v2.tsx

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

+ 104 - 14
packages/ui/src/v2/components/select-v2.css

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

+ 3 - 2
packages/ui/src/v2/components/select-v2.stories.tsx

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

+ 16 - 4
packages/ui/src/v2/components/select-v2.tsx

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