select.tsx 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import { Select as Kobalte } from "@kobalte/core/select"
  2. import { createMemo, type ComponentProps } from "solid-js"
  3. import { Icon, Button, type ButtonProps } from "@opencode-ai/ui"
  4. import { pipe, groupBy, entries, map } from "remeda"
  5. export interface SelectProps<T> {
  6. placeholder?: string
  7. options: T[]
  8. current?: T
  9. value?: (x: T) => string
  10. label?: (x: T) => string
  11. groupBy?: (x: T) => string
  12. onSelect?: (value: T | undefined) => void
  13. class?: ComponentProps<"div">["class"]
  14. classList?: ComponentProps<"div">["classList"]
  15. }
  16. export function Select<T>(props: SelectProps<T> & ButtonProps) {
  17. const grouped = createMemo(() => {
  18. const result = pipe(
  19. props.options,
  20. groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
  21. // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))),
  22. entries(),
  23. map(([k, v]) => ({ category: k, options: v })),
  24. )
  25. return result
  26. })
  27. return (
  28. <Kobalte<T, { category: string; options: T[] }>
  29. data-component="select"
  30. value={props.current}
  31. options={grouped()}
  32. optionValue={(x) => (props.value ? props.value(x) : (x as string))}
  33. optionTextValue={(x) => (props.label ? props.label(x) : (x as string))}
  34. optionGroupChildren="options"
  35. placeholder={props.placeholder}
  36. sectionComponent={(props) => (
  37. <Kobalte.Section data-slot="section">{props.section.rawValue.category}</Kobalte.Section>
  38. )}
  39. itemComponent={(itemProps) => (
  40. <Kobalte.Item
  41. data-slot="item"
  42. classList={{
  43. ...(props.classList ?? {}),
  44. [props.class ?? ""]: !!props.class,
  45. }}
  46. {...itemProps}
  47. >
  48. <Kobalte.ItemLabel data-slot="item-label">
  49. {props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)}
  50. </Kobalte.ItemLabel>
  51. <Kobalte.ItemIndicator data-slot="item-indicator">
  52. <Icon name="check-small" size="small" />
  53. </Kobalte.ItemIndicator>
  54. </Kobalte.Item>
  55. )}
  56. onChange={(v) => {
  57. props.onSelect?.(v ?? undefined)
  58. }}
  59. >
  60. <Kobalte.Trigger
  61. data-slot="trigger"
  62. as={Button}
  63. size={props.size}
  64. variant={props.variant}
  65. classList={{
  66. ...(props.classList ?? {}),
  67. [props.class ?? ""]: !!props.class,
  68. }}
  69. >
  70. <Kobalte.Value<T> data-slot="value">
  71. {(state) => {
  72. const selected = state.selectedOption() ?? props.current
  73. if (!selected) return props.placeholder || ""
  74. if (props.label) return props.label(selected)
  75. return selected as string
  76. }}
  77. </Kobalte.Value>
  78. <Kobalte.Icon data-slot="icon">
  79. <Icon name="chevron-down" size="small" />
  80. </Kobalte.Icon>
  81. </Kobalte.Trigger>
  82. <Kobalte.Portal>
  83. <Kobalte.Content
  84. classList={{
  85. ...(props.classList ?? {}),
  86. [props.class ?? ""]: !!props.class,
  87. }}
  88. data-component="select-content"
  89. >
  90. <Kobalte.Listbox data-slot="list" />
  91. </Kobalte.Content>
  92. </Kobalte.Portal>
  93. </Kobalte>
  94. )
  95. }