select.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import { Select as KobalteSelect } from "@kobalte/core/select"
  2. import { createMemo } from "solid-js"
  3. import type { ComponentProps } from "solid-js"
  4. import { Icon } from "@/ui/icon"
  5. import { pipe, groupBy, entries, map } from "remeda"
  6. import { Button, type ButtonProps } from "@/ui"
  7. export interface SelectProps<T> {
  8. placeholder?: string
  9. options: T[]
  10. current?: T
  11. value?: (x: T) => string
  12. label?: (x: T) => string
  13. groupBy?: (x: T) => string
  14. onSelect?: (value: T | undefined) => void
  15. class?: ComponentProps<"div">["class"]
  16. classList?: ComponentProps<"div">["classList"]
  17. }
  18. export function Select<T>(props: SelectProps<T> & ButtonProps) {
  19. const grouped = createMemo(() => {
  20. const result = pipe(
  21. props.options,
  22. groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
  23. // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))),
  24. entries(),
  25. map(([k, v]) => ({ category: k, options: v })),
  26. )
  27. return result
  28. })
  29. return (
  30. <KobalteSelect<T, { category: string; options: T[] }>
  31. value={props.current}
  32. options={grouped()}
  33. optionValue={(x) => (props.value ? props.value(x) : (x as string))}
  34. optionTextValue={(x) => (props.label ? props.label(x) : (x as string))}
  35. optionGroupChildren="options"
  36. placeholder={props.placeholder}
  37. sectionComponent={(props) => (
  38. <KobalteSelect.Section class="text-xs uppercase text-text-muted/60 font-light mt-3 first:mt-0 ml-2">
  39. {props.section.rawValue.category}
  40. </KobalteSelect.Section>
  41. )}
  42. itemComponent={(itemProps) => (
  43. <KobalteSelect.Item
  44. classList={{
  45. "relative flex cursor-pointer select-none items-center": true,
  46. "rounded-sm px-2 py-0.5 text-xs outline-none text-text": true,
  47. "transition-colors data-[disabled]:pointer-events-none": true,
  48. "data-[highlighted]:bg-background-element data-[disabled]:opacity-50": true,
  49. [props.class ?? ""]: !!props.class,
  50. }}
  51. {...itemProps}
  52. >
  53. <KobalteSelect.ItemLabel>
  54. {props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)}
  55. </KobalteSelect.ItemLabel>
  56. <KobalteSelect.ItemIndicator class="ml-auto">
  57. <Icon name="checkmark" size={16} />
  58. </KobalteSelect.ItemIndicator>
  59. </KobalteSelect.Item>
  60. )}
  61. onChange={(v) => {
  62. props.onSelect?.(v ?? undefined)
  63. }}
  64. >
  65. <KobalteSelect.Trigger
  66. as={Button}
  67. size={props.size || "sm"}
  68. variant={props.variant || "secondary"}
  69. classList={{
  70. ...(props.classList ?? {}),
  71. [props.class ?? ""]: !!props.class,
  72. }}
  73. >
  74. <KobalteSelect.Value<T>>
  75. {(state) => {
  76. const selected = state.selectedOption() ?? props.current
  77. if (!selected) return props.placeholder || ""
  78. if (props.label) return props.label(selected)
  79. return selected as string
  80. }}
  81. </KobalteSelect.Value>
  82. <KobalteSelect.Icon
  83. classList={{
  84. "size-fit shrink-0 text-text-muted transition-transform duration-100 data-[expanded]:rotate-180": true,
  85. }}
  86. >
  87. <Icon name="chevron-down" size={24} />
  88. </KobalteSelect.Icon>
  89. </KobalteSelect.Trigger>
  90. <KobalteSelect.Portal>
  91. <KobalteSelect.Content
  92. classList={{
  93. "min-w-32 overflow-hidden rounded-md border border-border-subtle/40": true,
  94. "bg-background-panel p-1 shadow-md z-50": true,
  95. "data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95": true,
  96. "data-[expanded]:animate-in data-[expanded]:fade-in-0 data-[expanded]:zoom-in-95": true,
  97. }}
  98. >
  99. <KobalteSelect.Listbox class="overflow-y-auto max-h-48" />
  100. </KobalteSelect.Content>
  101. </KobalteSelect.Portal>
  102. </KobalteSelect>
  103. )
  104. }