solid-element-ui 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/{src/alert-dialog → alert-dialog}/alert-dialog.d.ts +1 -1
  2. package/dist/{src/badge → badge}/badge.d.ts +2 -2
  3. package/dist/index.css +3 -1
  4. package/dist/index.js +16097 -17258
  5. package/dist/{src/separator → separator}/separator.d.ts +11 -5
  6. package/dist/{src/skeleton → skeleton}/skeleton.d.ts +2 -2
  7. package/dist/{src/slider → slider}/slider.d.ts +12 -0
  8. package/dist/{src/toast → toast}/toast.d.ts +3 -1
  9. package/dist/{src/toggle-button → toggle-button}/toggle-button.d.ts +0 -3
  10. package/package.json +11 -7
  11. package/src/accordion/accordion.tsx +80 -0
  12. package/src/alert/alert.tsx +86 -0
  13. package/src/alert-dialog/alert-dialog.tsx +129 -0
  14. package/src/badge/badge.tsx +49 -0
  15. package/src/breadcrumbs/breadcrumbs.tsx +69 -0
  16. package/src/button/button.tsx +216 -0
  17. package/src/checkbox/checkbox.tsx +63 -0
  18. package/src/collapsible/collapsible.tsx +46 -0
  19. package/src/color-area/color-area.tsx +46 -0
  20. package/src/color-channel-field/color-channel-field.tsx +46 -0
  21. package/src/color-field/color-field.tsx +64 -0
  22. package/src/color-slider/color-slider.tsx +60 -0
  23. package/src/color-swatch/color-swatch.tsx +33 -0
  24. package/src/color-wheel/color-wheel.tsx +50 -0
  25. package/src/combobox/combobox.tsx +97 -0
  26. package/src/context-menu/context-menu.tsx +102 -0
  27. package/src/dialog/dialog.tsx +101 -0
  28. package/src/dropdown-menu/dropdown-menu.tsx +111 -0
  29. package/src/file-field/file-field.tsx +114 -0
  30. package/src/hover-card/hover-card.tsx +61 -0
  31. package/src/image/image.tsx +59 -0
  32. package/src/index.tsx +94 -0
  33. package/src/link/link.tsx +64 -0
  34. package/src/menubar/menubar.tsx +85 -0
  35. package/src/meter/meter.tsx +89 -0
  36. package/src/navigation-menu/navigation-menu.tsx +90 -0
  37. package/src/number-field/number-field.tsx +79 -0
  38. package/src/pagination/pagination.tsx +67 -0
  39. package/src/popover/popover.tsx +58 -0
  40. package/src/progress/progress.tsx +83 -0
  41. package/src/radio-group/radio-group.tsx +94 -0
  42. package/src/rating-group/rating-group.tsx +101 -0
  43. package/src/search/search.tsx +99 -0
  44. package/src/segmented-control/segmented-control.tsx +92 -0
  45. package/src/select/select.tsx +163 -0
  46. package/src/separator/separator.tsx +64 -0
  47. package/src/skeleton/skeleton.tsx +73 -0
  48. package/src/slider/slider.tsx +94 -0
  49. package/src/style/global.css +156 -0
  50. package/src/switch/switch.tsx +104 -0
  51. package/src/tabs/tabs.tsx +73 -0
  52. package/src/text-field/text-field.tsx +97 -0
  53. package/src/time-field/time-field.tsx +103 -0
  54. package/src/toast/toast.tsx +132 -0
  55. package/src/toggle-button/toggle-button.tsx +69 -0
  56. package/src/toggle-group/toggle-group.tsx +85 -0
  57. package/src/tooltip/tooltip.tsx +73 -0
  58. /package/dist/{src/accordion → accordion}/accordion.d.ts +0 -0
  59. /package/dist/{src/alert → alert}/alert.d.ts +0 -0
  60. /package/dist/{src/breadcrumbs → breadcrumbs}/breadcrumbs.d.ts +0 -0
  61. /package/dist/{src/button → button}/button.d.ts +0 -0
  62. /package/dist/{src/checkbox → checkbox}/checkbox.d.ts +0 -0
  63. /package/dist/{src/collapsible → collapsible}/collapsible.d.ts +0 -0
  64. /package/dist/{src/color-area → color-area}/color-area.d.ts +0 -0
  65. /package/dist/{src/color-channel-field → color-channel-field}/color-channel-field.d.ts +0 -0
  66. /package/dist/{src/color-field → color-field}/color-field.d.ts +0 -0
  67. /package/dist/{src/color-slider → color-slider}/color-slider.d.ts +0 -0
  68. /package/dist/{src/color-swatch → color-swatch}/color-swatch.d.ts +0 -0
  69. /package/dist/{src/color-wheel → color-wheel}/color-wheel.d.ts +0 -0
  70. /package/dist/{src/combobox → combobox}/combobox.d.ts +0 -0
  71. /package/dist/{src/context-menu → context-menu}/context-menu.d.ts +0 -0
  72. /package/dist/{src/dialog → dialog}/dialog.d.ts +0 -0
  73. /package/dist/{src/dropdown-menu → dropdown-menu}/dropdown-menu.d.ts +0 -0
  74. /package/dist/{src/file-field → file-field}/file-field.d.ts +0 -0
  75. /package/dist/{src/hover-card → hover-card}/hover-card.d.ts +0 -0
  76. /package/dist/{src/image → image}/image.d.ts +0 -0
  77. /package/dist/{src/index.d.ts → index.d.ts} +0 -0
  78. /package/dist/{src/link → link}/link.d.ts +0 -0
  79. /package/dist/{src/menubar → menubar}/menubar.d.ts +0 -0
  80. /package/dist/{src/meter → meter}/meter.d.ts +0 -0
  81. /package/dist/{src/navigation-menu → navigation-menu}/navigation-menu.d.ts +0 -0
  82. /package/dist/{src/number-field → number-field}/number-field.d.ts +0 -0
  83. /package/dist/{src/pagination → pagination}/pagination.d.ts +0 -0
  84. /package/dist/{src/popover → popover}/popover.d.ts +0 -0
  85. /package/dist/{src/progress → progress}/progress.d.ts +0 -0
  86. /package/dist/{src/radio-group → radio-group}/radio-group.d.ts +0 -0
  87. /package/dist/{src/rating-group → rating-group}/rating-group.d.ts +0 -0
  88. /package/dist/{src/search → search}/search.d.ts +0 -0
  89. /package/dist/{src/segmented-control → segmented-control}/segmented-control.d.ts +0 -0
  90. /package/dist/{src/select → select}/select.d.ts +0 -0
  91. /package/dist/{src/switch → switch}/switch.d.ts +0 -0
  92. /package/dist/{src/tabs → tabs}/tabs.d.ts +0 -0
  93. /package/dist/{src/text-field → text-field}/text-field.d.ts +0 -0
  94. /package/dist/{src/time-field → time-field}/time-field.d.ts +0 -0
  95. /package/dist/{src/toggle-group → toggle-group}/toggle-group.d.ts +0 -0
  96. /package/dist/{src/tooltip → tooltip}/tooltip.d.ts +0 -0
@@ -0,0 +1,83 @@
1
+ import { Progress as KProgress } from "@kobalte/core/progress";
2
+ import { splitProps, type ComponentProps, Show } from "solid-js";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ // FIXME 进度条问题,value 直接占满
6
+
7
+ const progressStyles = tv(
8
+ {
9
+ slots: {
10
+ root: "flex flex-col gap-2 w-full antialiased",
11
+ labelContainer:
12
+ "flex justify-between items-center text-sm font-medium text-slate-700 dark:text-slate-300",
13
+ track: "h-2 w-full rounded-full bg-appoverflow-hidden",
14
+ fill: "h-full bg-primary transition-all duration-300 ease-in-out data-[indeterminate]:animate-progress-indeterminate",
15
+ },
16
+ variants: {
17
+ size: {
18
+ sm: { track: "h-1" },
19
+ md: { track: "h-2" },
20
+ lg: { track: "h-3" },
21
+ },
22
+ radius: {
23
+ none: { track: "rounded-none", fill: "rounded-none" },
24
+ sm: { track: "rounded-sm", fill: "rounded-sm" },
25
+ md: { track: "rounded-md", fill: "rounded-md" },
26
+ lg: { track: "rounded-lg", fill: "rounded-lg" },
27
+ full: { track: "rounded-full", fill: "rounded-full" },
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ size: "md",
32
+ radius: "full",
33
+ },
34
+ },
35
+ {
36
+ twMerge: true,
37
+ },
38
+ );
39
+
40
+ type ProgressVariants = VariantProps<typeof progressStyles>;
41
+
42
+ export interface ProgressProps
43
+ extends Omit<ComponentProps<typeof KProgress>, "children">,
44
+ ProgressVariants {
45
+ label?: string;
46
+ showValue?: boolean;
47
+ }
48
+
49
+
50
+ export const Progress = (props: ProgressProps) => {
51
+ const [local, variantProps, others] = splitProps(
52
+ props,
53
+ ["label", "showValue", "class"],
54
+ ["size", "radius"]
55
+ );
56
+
57
+ const { root, labelContainer, track, fill } = progressStyles({
58
+ size: variantProps.size,
59
+ radius: variantProps.radius,
60
+ });
61
+
62
+ return (
63
+ <KProgress class={root({ class: local.class })} {...others}>
64
+ <Show when={local.label || local.showValue}>
65
+ <div class={labelContainer()}>
66
+ <Show when={local.label}>
67
+ <KProgress.Label>{local.label}</KProgress.Label>
68
+ </Show>
69
+ <Show when={local.showValue}>
70
+ <KProgress.ValueLabel class="text-xs text-slate-500" />
71
+ </Show>
72
+ </div>
73
+ </Show>
74
+
75
+ <KProgress.Track class={track()}>
76
+ <KProgress.Fill
77
+ class={fill()}
78
+ style={{ width: "var(--kb-progress-fill-width)" }}
79
+ />
80
+ </KProgress.Track>
81
+ </KProgress>
82
+ );
83
+ };
@@ -0,0 +1,94 @@
1
+ import { RadioGroup as KRadioGroup } from "@kobalte/core/radio-group";
2
+ import { splitProps, type ComponentProps, For, Show } from "solid-js";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ const radioStyles = tv(
6
+ {
7
+ slots: {
8
+ root: "flex flex-col gap-3 antialiased",
9
+ label: "text-sm font-semibold text-main mb-1",
10
+ item: "group flex items-center gap-3 cursor-pointer disabled:cursor-not-allowed",
11
+ control: [
12
+ "flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-slate-300 bg-app transition-all shadow-sm",
13
+ "group-hover:border-light group-data-[checked]:border-primary group-data-[checked]:bg-primary",
14
+ "group-focus-visible:ring-2 group-focus-visible:ring-blue-500/20",
15
+
16
+ ],
17
+ indicator: "h-2 w-2 rounded-full bg-app shadow-sm",
18
+ itemLabel:
19
+ "text-sm font-medium text-main group-data-[disabled]:opacity-50",
20
+ },
21
+ variants: {
22
+ orientation: {
23
+ horizontal: { root: "flex-row flex-wrap gap-6" },
24
+ vertical: { root: "flex-col" },
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ orientation: "vertical",
29
+ },
30
+ },
31
+ {
32
+ twMerge: true,
33
+ },
34
+ );
35
+
36
+ type RadioVariants = VariantProps<typeof radioStyles>;
37
+
38
+ export interface RadioOption {
39
+ label: string;
40
+ value: string;
41
+ disabled?: boolean;
42
+ }
43
+
44
+ export interface RadioGroupProps
45
+ extends Omit<ComponentProps<typeof KRadioGroup>, "children" | "class">,
46
+ RadioVariants {
47
+ label?: string;
48
+ options: RadioOption[];
49
+ class?: string;
50
+ }
51
+
52
+ /**
53
+ * RadioGroup 高度封装版
54
+ * 自动处理循环渲染、选中指示器以及水平/垂直布局
55
+ */
56
+ export const RadioGroup = (props: RadioGroupProps) => {
57
+ const [local, variantProps, others] = splitProps(
58
+ props,
59
+ ["label", "options", "class"],
60
+ ["orientation"]
61
+ );
62
+
63
+ const s = () => radioStyles({ orientation: variantProps.orientation });
64
+
65
+ return (
66
+ <KRadioGroup class={s().root({ class: local.class })} {...others}>
67
+ <Show when={local.label}>
68
+ <KRadioGroup.Label class={s().label()}>
69
+ {local.label}
70
+ </KRadioGroup.Label>
71
+ </Show>
72
+
73
+ <For each={local.options}>
74
+ {(option) => (
75
+ <KRadioGroup.Item
76
+ value={option.value}
77
+ disabled={option.disabled}
78
+ class={s().item()}
79
+ >
80
+ <KRadioGroup.ItemInput />
81
+ <KRadioGroup.ItemControl class={s().control()}>
82
+ <KRadioGroup.ItemIndicator
83
+ class={s().indicator()}
84
+ />
85
+ </KRadioGroup.ItemControl>
86
+ <KRadioGroup.ItemLabel class={s().itemLabel()}>
87
+ {option.label}
88
+ </KRadioGroup.ItemLabel>
89
+ </KRadioGroup.Item>
90
+ )}
91
+ </For>
92
+ </KRadioGroup>
93
+ );
94
+ };
@@ -0,0 +1,101 @@
1
+ // import { RatingGroup as KRatingGroup } from "@kobalte/core/rating-group";
2
+ // import { splitProps, type ComponentProps, For, Show } from "solid-js";
3
+ // import { tv, type VariantProps } from "tailwind-variants";
4
+ // import { Star } from "lucide-solid";
5
+
6
+ // const ratingStyles = tv({
7
+ // slots: {
8
+ // root: "flex flex-col gap-1.5 antialiased",
9
+ // label: "text-sm font-medium text-slate-700 dark:text-slate-300",
10
+ // control: "flex items-center gap-0.5",
11
+ // item: "relative cursor-pointer transition-transform active:scale-90 focus-visible:outline-none",
12
+ // itemIndicator: "flex items-center justify-center transition-colors",
13
+ // },
14
+ // variants: {
15
+ // color: {
16
+ // yellow: {
17
+ // itemIndicator:
18
+ // "text-amber-400 fill-transparent data-[highlighted]:fill-amber-400 data-[highlighted]:text-amber-400",
19
+ // },
20
+ // blue: {
21
+ // itemIndicator:
22
+ // "text-blue-500 fill-transparent data-[highlighted]:fill-blue-500 data-[highlighted]:text-blue-500",
23
+ // },
24
+ // red: {
25
+ // itemIndicator:
26
+ // "text-red-500 fill-transparent data-[highlighted]:fill-red-500 data-[highlighted]:text-red-500",
27
+ // },
28
+ // },
29
+ // size: {
30
+ // sm: { itemIndicator: "h-4 w-4" },
31
+ // md: { itemIndicator: "h-6 w-6" },
32
+ // lg: { itemIndicator: "h-8 w-8" },
33
+ // },
34
+ // },
35
+ // defaultVariants: {
36
+ // color: "yellow",
37
+ // size: "md",
38
+ // },
39
+ // });
40
+
41
+ // type RatingVariants = VariantProps<typeof ratingStyles>;
42
+
43
+ // export interface RatingGroupProps
44
+ // extends Omit<ComponentProps<typeof KRatingGroup>, "children" | "class">,
45
+ // RatingVariants {
46
+ // label?: string;
47
+ // count?: number; // 星星总数,默认为 5
48
+ // class?: string;
49
+ // }
50
+
51
+ // /**
52
+ // * RatingGroup 高度封装版
53
+ // * 自动处理星星循环、高亮逻辑及表单集成
54
+ // */
55
+ // export const RatingGroup = (props: RatingGroupProps) => {
56
+ // const [local, variantProps, others] = splitProps(
57
+ // props,
58
+ // ["label", "count", "class"],
59
+ // ["color", "size"]
60
+ // );
61
+
62
+ // const s = () =>
63
+ // ratingStyles({
64
+ // color: variantProps.color,
65
+ // size: variantProps.size,
66
+ // });
67
+
68
+ // const maxCount = () => local.count ?? 5;
69
+
70
+ // return (
71
+ // <KProgress class={s().root({ class: local.class })} {...others}>
72
+ // <Show when={local.label}>
73
+ // <KRatingGroup.Label class={s().label()}>
74
+ // {local.label}
75
+ // </KRatingGroup.Label>
76
+ // </Show>
77
+
78
+ // <KRatingGroup.Control class={s().control()}>
79
+ // <KRatingGroup.HiddenInput />
80
+ // <KRatingGroup.Context>
81
+ // {(state) => (
82
+ // <For each={state.items}>
83
+ // {(index) => (
84
+ // <KRatingGroup.Item
85
+ // index={index}
86
+ // class={s().item()}
87
+ // >
88
+ // <KRatingGroup.ItemIndicator
89
+ // class={s().itemIndicator()}
90
+ // >
91
+ // <Star />
92
+ // </KRatingGroup.ItemIndicator>
93
+ // </KRatingGroup.Item>
94
+ // )}
95
+ // </For>
96
+ // )}
97
+ // </KRatingGroup.Context>
98
+ // </KRatingGroup.Control>
99
+ // </KProgress>
100
+ // );
101
+ // };
@@ -0,0 +1,99 @@
1
+ import { TextField as KSearch } from "@kobalte/core/text-field";
2
+ import { splitProps, type ComponentProps, Show } from "solid-js";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+ import { Search as SearchIcon, CircleX } from "lucide-solid";
5
+
6
+ const searchStyles = tv(
7
+ {
8
+ slots: {
9
+ root: "relative flex flex-col gap-1.5 w-full",
10
+ inputWrapper: "relative flex items-center transition-all",
11
+ input: [
12
+ "flex h-10 w-full rounded-md border border-light bg-app px-9 py-2 text-sm",
13
+ "ring-offset-app file:border-0 file:bg-transparent file:text-sm file:font-medium",
14
+ "placeholder:text-muted focus-visible:outline-none focus-visible:ring-2",
15
+ "disabled:cursor-not-allowed disabled:opacity-50",
16
+ ],
17
+ icon: "absolute left-3 h-4 w-4 text-main pointer-events-none",
18
+ clear: "absolute right-3 h-4 w-4 text-main hover:text-muted cursor-pointer transition-colors",
19
+ },
20
+ variants: {
21
+ size: {
22
+ sm: {
23
+ input: "h-8 text-xs px-8",
24
+ icon: "h-3.5 w-3.5",
25
+ clear: "h-3.5 w-3.5",
26
+ },
27
+ md: {
28
+ input: "h-10 text-sm",
29
+ icon: "h-4 w-4",
30
+ clear: "h-4 w-4",
31
+ },
32
+ lg: {
33
+ input: "h-12 text-base px-10",
34
+ icon: "h-5 w-5",
35
+ clear: "h-5 w-5",
36
+ },
37
+ },
38
+ ringColor: {
39
+ primary: { input: "focus-visible:ring-primary" },
40
+ danger: { input: "focus-visible:ring-danger" },
41
+ },
42
+ },
43
+ defaultVariants: {
44
+ size: "md",
45
+ ringColor: "primary",
46
+ },
47
+ },
48
+ {
49
+ twMerge: true,
50
+ },
51
+ );
52
+
53
+ type SearchVariants = VariantProps<typeof searchStyles>;
54
+
55
+ export interface SearchProps
56
+ extends Omit<ComponentProps<typeof KSearch>, "class">, SearchVariants {
57
+ class?: string;
58
+ placeholder?: string;
59
+ allowClear?: boolean;
60
+ onClear?: () => void;
61
+ }
62
+
63
+ export const Search = (props: SearchProps) => {
64
+ const [local, variantProps, others] = splitProps(
65
+ props,
66
+ ["class", "placeholder", "allowClear", "onClear", "value", "onChange"],
67
+ ["size", "ringColor"],
68
+ );
69
+
70
+ const styles = searchStyles(variantProps);
71
+
72
+ return (
73
+ <KSearch
74
+ class={styles.root({ class: local.class })}
75
+ value={local.value}
76
+ onChange={local.onChange}
77
+ {...others}
78
+ >
79
+ <div class={styles.inputWrapper()}>
80
+ <SearchIcon class={styles.icon()} />
81
+ <KSearch.Input
82
+ class={styles.input()}
83
+ placeholder={local.placeholder ?? "搜索..."}
84
+ />
85
+ <Show when={local.allowClear && local.value}>
86
+ <button
87
+ onClick={() => local.onClear?.()}
88
+ class={styles.clear()}
89
+ >
90
+ <CircleX
91
+ fill="currentColor"
92
+ class="text-white dark:text-slate-950"
93
+ />
94
+ </button>
95
+ </Show>
96
+ </div>
97
+ </KSearch>
98
+ );
99
+ };
@@ -0,0 +1,92 @@
1
+ import { SegmentedControl as KSegmented } from "@kobalte/core/segmented-control";
2
+ import { splitProps, For, type ComponentProps } from "solid-js";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ // FIXME 样式问题
6
+
7
+ const segmentedStyles = tv(
8
+ {
9
+ slots: {
10
+ root: "relative flex flex-col",
11
+ container:
12
+ "relative flex items-center w-full rounded-lg bg-foreground p-1 text-main",
13
+ item: [
14
+ " z-10 inline-flex flex-1 items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-all cursor-pointer",
15
+ "outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2",
16
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
17
+ " transition-colors duration-200",
18
+ ],
19
+ indicator:
20
+ "absolute z-0 bg-app shadow-sm rounded-md transition-all duration-200 ease-in-out ",
21
+ label: "mb-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
22
+ itemLabel: "flex w-full cursor-pointer items-center justify-center",
23
+ },
24
+ variants: {
25
+ size: {
26
+ sm: { root: "p-0.5", item: "px-2 py-1 text-xs" },
27
+ md: { root: "p-1", item: "px-3 py-1.5 text-sm" },
28
+ lg: { root: "p-1.5", item: "px-6 py-2 text-base" },
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ size: "md",
33
+ },
34
+ },
35
+ {
36
+ twMerge: true,
37
+ },
38
+ );
39
+
40
+ type SegmentedVariants = VariantProps<typeof segmentedStyles>;
41
+
42
+ interface Option {
43
+ label: string;
44
+ value: string;
45
+ disabled?: boolean;
46
+ }
47
+
48
+ export interface SegmentedControlProps
49
+ extends
50
+ Omit<ComponentProps<typeof KSegmented>, "class">,
51
+ SegmentedVariants {
52
+ options: Option[];
53
+ label?: string;
54
+ class?: string;
55
+ }
56
+
57
+ export const SegmentedControl = (props: SegmentedControlProps) => {
58
+ const [local, variantProps, others] = splitProps(
59
+ props,
60
+ ["options", "class", "label"],
61
+ ["size"],
62
+ );
63
+
64
+ const styles = segmentedStyles(variantProps);
65
+
66
+ return (
67
+ <KSegmented class={styles.root({ class: local.class })} {...others}>
68
+ {local.label && (
69
+ <KSegmented.Label class={styles.label()}>
70
+ {local.label}
71
+ </KSegmented.Label>
72
+ )}
73
+ <div class={styles.container()}>
74
+ <For each={local.options}>
75
+ {(option) => (
76
+ <KSegmented.Item
77
+ value={option.value}
78
+ disabled={option.disabled}
79
+ class={styles.item()}
80
+ >
81
+ <KSegmented.ItemInput />
82
+ <KSegmented.ItemLabel class={styles.itemLabel()}>
83
+ {option.label}
84
+ </KSegmented.ItemLabel>
85
+ </KSegmented.Item>
86
+ )}
87
+ </For>
88
+ <KSegmented.Indicator class={styles.indicator()} />
89
+ </div>
90
+ </KSegmented>
91
+ );
92
+ };
@@ -0,0 +1,163 @@
1
+ import { Select as KSelect } from "@kobalte/core/select";
2
+ import { splitProps, Show, createMemo } from "solid-js";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+ import { ChevronDown, Check } from "lucide-solid";
5
+
6
+ const selectStyles = tv(
7
+ {
8
+ slots: {
9
+ root: "flex flex-col gap-1.5 w-full",
10
+ label: "text-sm font-medium text-muted",
11
+ trigger: [
12
+ "flex h-10 w-full text-main items-center justify-between rounded-md border border-light bg-app px-3 py-2 text-sm",
13
+ "ring-offset-white placeholder:text-muted focus:outline-none focus:ring-1 focus:ring-blue-500",
14
+ "disabled:cursor-not-allowed disabled:opacity-50",
15
+ ],
16
+ content: [
17
+ "relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-light bg-app shadow-md text-main",
18
+ "data-[expanded]:animate-in data-[closed]:animate-out",
19
+ ],
20
+ listbox: "p-1",
21
+ item: [
22
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
23
+ "focus:bg-foreground focus:text-main data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
24
+ ],
25
+ itemIndicator:
26
+ "absolute left-2 flex h-3.5 w-3.5 items-center justify-center",
27
+ description: "mt-1 text-xs text-muted",
28
+ },
29
+ variants: {
30
+ size: {
31
+ sm: { trigger: "h-8 text-xs", item: "py-1 text-xs" },
32
+ md: { trigger: "h-10 text-sm", item: "py-1.5 text-sm" },
33
+ lg: { trigger: "h-12 text-base", item: "py-2 text-base" },
34
+ },
35
+ },
36
+ defaultVariants: {
37
+ size: "md",
38
+ },
39
+ },
40
+ {
41
+ twMerge: true,
42
+ },
43
+ );
44
+
45
+ type SelectVariants = VariantProps<typeof selectStyles>;
46
+
47
+ interface Option {
48
+ label: string;
49
+ value: string;
50
+ disabled?: boolean;
51
+ }
52
+
53
+ // 重新定义接口,使 value 和 onChange 处理的是 string 类型
54
+ export interface SelectProps extends SelectVariants {
55
+ options: Option[];
56
+ value?: string;
57
+ defaultValue?: string;
58
+ onChange?: (value: string) => void;
59
+ label?: string;
60
+ description?: string;
61
+ placeholder?: string;
62
+ disabled?: boolean;
63
+ name?: string;
64
+ class?: string;
65
+ }
66
+
67
+ export const Select = (props: SelectProps) => {
68
+ const [local, variantProps, others] = splitProps(
69
+ props,
70
+ [
71
+ "options",
72
+ "label",
73
+ "description",
74
+ "placeholder",
75
+ "class",
76
+ "value",
77
+ "defaultValue",
78
+ "onChange",
79
+ ],
80
+ ["size"],
81
+ );
82
+
83
+ const styles = selectStyles(variantProps);
84
+
85
+ const selectedOption = createMemo(() => {
86
+ if (local.value === undefined) return undefined;
87
+ return local.options.find((opt) => opt.value === local.value);
88
+ });
89
+
90
+ const defaultOption = createMemo(() => {
91
+ if (local.defaultValue === undefined) return undefined;
92
+ return local.options.find((opt) => opt.value === local.defaultValue);
93
+ });
94
+
95
+ const handleValueChange = (opt: Option | null) => {
96
+ if (opt === null) {
97
+
98
+ local.onChange?.("");
99
+ return;
100
+ }
101
+ local.onChange?.(opt.value);
102
+ };
103
+
104
+ return (
105
+ <KSelect<Option>
106
+ multiple={false}
107
+ options={local.options}
108
+ optionValue="value"
109
+ optionTextValue="label"
110
+ optionDisabled="disabled"
111
+ placeholder={local.placeholder}
112
+ value={selectedOption()}
113
+ defaultValue={defaultOption()}
114
+ onChange={handleValueChange}
115
+ class={styles.root({ class: local.class })}
116
+ {...others}
117
+ itemComponent={(itemProps) => (
118
+ <KSelect.Item item={itemProps.item} class={styles.item()}>
119
+ <KSelect.ItemIndicator class={styles.itemIndicator()}>
120
+ <Check size={14} />
121
+ </KSelect.ItemIndicator>
122
+ <KSelect.ItemLabel>
123
+ {itemProps.item.textValue}
124
+ </KSelect.ItemLabel>
125
+ </KSelect.Item>
126
+ )}
127
+ >
128
+ <Show when={local.label}>
129
+ <KSelect.Label class={styles.label()}>
130
+ {local.label}
131
+ </KSelect.Label>
132
+ </Show>
133
+
134
+ <KSelect.Trigger class={styles.trigger()}>
135
+ <KSelect.Value<Option>>
136
+ {(state) => (
137
+ <Show
138
+ when={state.selectedOption()}
139
+ fallback={local.placeholder}
140
+ >
141
+ {state.selectedOption()?.label}
142
+ </Show>
143
+ )}
144
+ </KSelect.Value>
145
+ <KSelect.Icon>
146
+ <ChevronDown size={16} class="opacity-50" />
147
+ </KSelect.Icon>
148
+ </KSelect.Trigger>
149
+
150
+ <KSelect.Portal>
151
+ <KSelect.Content class={styles.content()}>
152
+ <KSelect.Listbox class={styles.listbox()} />
153
+ </KSelect.Content>
154
+ </KSelect.Portal>
155
+
156
+ <Show when={local.description}>
157
+ <KSelect.Description class={styles.description()}>
158
+ {local.description}
159
+ </KSelect.Description>
160
+ </Show>
161
+ </KSelect>
162
+ );
163
+ };