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.
- package/dist/{src/alert-dialog → alert-dialog}/alert-dialog.d.ts +1 -1
- package/dist/{src/badge → badge}/badge.d.ts +2 -2
- package/dist/index.css +3 -1
- package/dist/index.js +16097 -17258
- package/dist/{src/separator → separator}/separator.d.ts +11 -5
- package/dist/{src/skeleton → skeleton}/skeleton.d.ts +2 -2
- package/dist/{src/slider → slider}/slider.d.ts +12 -0
- package/dist/{src/toast → toast}/toast.d.ts +3 -1
- package/dist/{src/toggle-button → toggle-button}/toggle-button.d.ts +0 -3
- package/package.json +11 -7
- package/src/accordion/accordion.tsx +80 -0
- package/src/alert/alert.tsx +86 -0
- package/src/alert-dialog/alert-dialog.tsx +129 -0
- package/src/badge/badge.tsx +49 -0
- package/src/breadcrumbs/breadcrumbs.tsx +69 -0
- package/src/button/button.tsx +216 -0
- package/src/checkbox/checkbox.tsx +63 -0
- package/src/collapsible/collapsible.tsx +46 -0
- package/src/color-area/color-area.tsx +46 -0
- package/src/color-channel-field/color-channel-field.tsx +46 -0
- package/src/color-field/color-field.tsx +64 -0
- package/src/color-slider/color-slider.tsx +60 -0
- package/src/color-swatch/color-swatch.tsx +33 -0
- package/src/color-wheel/color-wheel.tsx +50 -0
- package/src/combobox/combobox.tsx +97 -0
- package/src/context-menu/context-menu.tsx +102 -0
- package/src/dialog/dialog.tsx +101 -0
- package/src/dropdown-menu/dropdown-menu.tsx +111 -0
- package/src/file-field/file-field.tsx +114 -0
- package/src/hover-card/hover-card.tsx +61 -0
- package/src/image/image.tsx +59 -0
- package/src/index.tsx +94 -0
- package/src/link/link.tsx +64 -0
- package/src/menubar/menubar.tsx +85 -0
- package/src/meter/meter.tsx +89 -0
- package/src/navigation-menu/navigation-menu.tsx +90 -0
- package/src/number-field/number-field.tsx +79 -0
- package/src/pagination/pagination.tsx +67 -0
- package/src/popover/popover.tsx +58 -0
- package/src/progress/progress.tsx +83 -0
- package/src/radio-group/radio-group.tsx +94 -0
- package/src/rating-group/rating-group.tsx +101 -0
- package/src/search/search.tsx +99 -0
- package/src/segmented-control/segmented-control.tsx +92 -0
- package/src/select/select.tsx +163 -0
- package/src/separator/separator.tsx +64 -0
- package/src/skeleton/skeleton.tsx +73 -0
- package/src/slider/slider.tsx +94 -0
- package/src/style/global.css +156 -0
- package/src/switch/switch.tsx +104 -0
- package/src/tabs/tabs.tsx +73 -0
- package/src/text-field/text-field.tsx +97 -0
- package/src/time-field/time-field.tsx +103 -0
- package/src/toast/toast.tsx +132 -0
- package/src/toggle-button/toggle-button.tsx +69 -0
- package/src/toggle-group/toggle-group.tsx +85 -0
- package/src/tooltip/tooltip.tsx +73 -0
- /package/dist/{src/accordion → accordion}/accordion.d.ts +0 -0
- /package/dist/{src/alert → alert}/alert.d.ts +0 -0
- /package/dist/{src/breadcrumbs → breadcrumbs}/breadcrumbs.d.ts +0 -0
- /package/dist/{src/button → button}/button.d.ts +0 -0
- /package/dist/{src/checkbox → checkbox}/checkbox.d.ts +0 -0
- /package/dist/{src/collapsible → collapsible}/collapsible.d.ts +0 -0
- /package/dist/{src/color-area → color-area}/color-area.d.ts +0 -0
- /package/dist/{src/color-channel-field → color-channel-field}/color-channel-field.d.ts +0 -0
- /package/dist/{src/color-field → color-field}/color-field.d.ts +0 -0
- /package/dist/{src/color-slider → color-slider}/color-slider.d.ts +0 -0
- /package/dist/{src/color-swatch → color-swatch}/color-swatch.d.ts +0 -0
- /package/dist/{src/color-wheel → color-wheel}/color-wheel.d.ts +0 -0
- /package/dist/{src/combobox → combobox}/combobox.d.ts +0 -0
- /package/dist/{src/context-menu → context-menu}/context-menu.d.ts +0 -0
- /package/dist/{src/dialog → dialog}/dialog.d.ts +0 -0
- /package/dist/{src/dropdown-menu → dropdown-menu}/dropdown-menu.d.ts +0 -0
- /package/dist/{src/file-field → file-field}/file-field.d.ts +0 -0
- /package/dist/{src/hover-card → hover-card}/hover-card.d.ts +0 -0
- /package/dist/{src/image → image}/image.d.ts +0 -0
- /package/dist/{src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{src/link → link}/link.d.ts +0 -0
- /package/dist/{src/menubar → menubar}/menubar.d.ts +0 -0
- /package/dist/{src/meter → meter}/meter.d.ts +0 -0
- /package/dist/{src/navigation-menu → navigation-menu}/navigation-menu.d.ts +0 -0
- /package/dist/{src/number-field → number-field}/number-field.d.ts +0 -0
- /package/dist/{src/pagination → pagination}/pagination.d.ts +0 -0
- /package/dist/{src/popover → popover}/popover.d.ts +0 -0
- /package/dist/{src/progress → progress}/progress.d.ts +0 -0
- /package/dist/{src/radio-group → radio-group}/radio-group.d.ts +0 -0
- /package/dist/{src/rating-group → rating-group}/rating-group.d.ts +0 -0
- /package/dist/{src/search → search}/search.d.ts +0 -0
- /package/dist/{src/segmented-control → segmented-control}/segmented-control.d.ts +0 -0
- /package/dist/{src/select → select}/select.d.ts +0 -0
- /package/dist/{src/switch → switch}/switch.d.ts +0 -0
- /package/dist/{src/tabs → tabs}/tabs.d.ts +0 -0
- /package/dist/{src/text-field → text-field}/text-field.d.ts +0 -0
- /package/dist/{src/time-field → time-field}/time-field.d.ts +0 -0
- /package/dist/{src/toggle-group → toggle-group}/toggle-group.d.ts +0 -0
- /package/dist/{src/tooltip → tooltip}/tooltip.d.ts +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Combobox as KCombobox } from "@kobalte/core/combobox";
|
|
2
|
+
import { splitProps, type ComponentProps } from "solid-js";
|
|
3
|
+
import { tv } from "tailwind-variants";
|
|
4
|
+
import { Check, ChevronDown } from "lucide-solid";
|
|
5
|
+
|
|
6
|
+
// FIXME 缺少Description,ErrorMessage,验证
|
|
7
|
+
|
|
8
|
+
const comboboxStyles = tv(
|
|
9
|
+
{
|
|
10
|
+
slots: {
|
|
11
|
+
root: "flex flex-col gap-1.5 w-full",
|
|
12
|
+
label: "text-sm font-medium text-zinc-900 dark:text-zinc-100 select-none",
|
|
13
|
+
control:
|
|
14
|
+
"relative flex items-center rounded-md border border-base bg-app shadow-sm transition-colors focus-within:ring-1 focus-within:ring-zinc-950 dark:focus-within:ring-zinc-300",
|
|
15
|
+
input: "h-9 w-full bg-transparent px-3 py-1 text-sm outline-none placeholder:text-zinc-500 disabled:cursor-not-allowed",
|
|
16
|
+
trigger: "flex h-9 w-9 items-center justify-center text-zinc-500",
|
|
17
|
+
content: [
|
|
18
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-base bg-app text-zinc-950 shadow-md animate-in zoom-in-95 dark:text-zinc-50",
|
|
19
|
+
"data-[expanded]:animate-in data-[closed]:animate-out",
|
|
20
|
+
],
|
|
21
|
+
listbox: "p-1",
|
|
22
|
+
item: "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-zinc-100 data-[highlighted]:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:data-[highlighted]:bg-zinc-800 dark:data-[highlighted]:text-zinc-50",
|
|
23
|
+
itemIndicator:
|
|
24
|
+
"absolute left-2 flex h-3.5 w-3.5 items-center justify-center",
|
|
25
|
+
icon: "h-4 w-4 transition-transform duration-200 origin-center data-[expanded]:rotate-180",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
twMerge: true,
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
root,
|
|
35
|
+
label,
|
|
36
|
+
control,
|
|
37
|
+
input,
|
|
38
|
+
trigger,
|
|
39
|
+
content,
|
|
40
|
+
listbox,
|
|
41
|
+
item,
|
|
42
|
+
itemIndicator,
|
|
43
|
+
icon,
|
|
44
|
+
} = comboboxStyles();
|
|
45
|
+
|
|
46
|
+
export type ComboboxProps<T> = ComponentProps<typeof KCombobox<T>> & {
|
|
47
|
+
label?: string;
|
|
48
|
+
placeholder?: string;
|
|
49
|
+
class?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const Combobox = <T extends string | object>(
|
|
53
|
+
props: ComboboxProps<T>,
|
|
54
|
+
) => {
|
|
55
|
+
const [local, others] = splitProps(props as ComboboxProps<T>, [
|
|
56
|
+
"label",
|
|
57
|
+
"placeholder",
|
|
58
|
+
"class",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<KCombobox<T> class={root({ class: local.class })} {...others}>
|
|
63
|
+
{local.label && (
|
|
64
|
+
<KCombobox.Label class={label()}>{local.label}</KCombobox.Label>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
<KCombobox.Control class={control()}>
|
|
68
|
+
<KCombobox.Input
|
|
69
|
+
class={input()}
|
|
70
|
+
placeholder={local.placeholder}
|
|
71
|
+
/>
|
|
72
|
+
<KCombobox.Trigger class={trigger()}>
|
|
73
|
+
<KCombobox.Icon class={icon()}>
|
|
74
|
+
<ChevronDown class="h-4 w-4" />
|
|
75
|
+
</KCombobox.Icon>
|
|
76
|
+
</KCombobox.Trigger>
|
|
77
|
+
</KCombobox.Control>
|
|
78
|
+
|
|
79
|
+
<KCombobox.Portal>
|
|
80
|
+
<KCombobox.Content class={content()}>
|
|
81
|
+
<KCombobox.Listbox class={listbox()} />
|
|
82
|
+
</KCombobox.Content>
|
|
83
|
+
</KCombobox.Portal>
|
|
84
|
+
</KCombobox>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const ComboboxItem = (props: { item: any }) => {
|
|
89
|
+
return (
|
|
90
|
+
<KCombobox.Item item={props.item} class={item()}>
|
|
91
|
+
<KCombobox.ItemIndicator class={itemIndicator()}>
|
|
92
|
+
<Check size={14} />
|
|
93
|
+
</KCombobox.ItemIndicator>
|
|
94
|
+
<KCombobox.ItemLabel>{props.item.rawValue}</KCombobox.ItemLabel>
|
|
95
|
+
</KCombobox.Item>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ContextMenu as KContextMenu } from "@kobalte/core/context-menu";
|
|
2
|
+
import { splitProps, For, Show,type JSX } from "solid-js";
|
|
3
|
+
import { tv } from "tailwind-variants";
|
|
4
|
+
import { ChevronRight } from "lucide-solid";
|
|
5
|
+
|
|
6
|
+
// TODO 样式修改
|
|
7
|
+
|
|
8
|
+
const menuStyles = tv(
|
|
9
|
+
{
|
|
10
|
+
slots: {
|
|
11
|
+
content: [
|
|
12
|
+
"z-50 min-w-[10rem] overflow-hidden rounded-md border border-light bg-app p-1 text-main shadow-md dark:text-zinc-50",
|
|
13
|
+
"data-[expanded]:animate-in data-[closed]:animate-out",
|
|
14
|
+
],
|
|
15
|
+
item: "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 ",
|
|
16
|
+
separator: "-mx-1 my-1 h-px border-light",
|
|
17
|
+
subIcon: "ml-auto h-4 w-4",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
twMerge: true,
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const { content, item, separator, subIcon } = menuStyles();
|
|
26
|
+
|
|
27
|
+
// 定义配置项类型
|
|
28
|
+
export type ContextMenuItemConfig = {
|
|
29
|
+
label: string;
|
|
30
|
+
onClick?: () => void;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
separator?: boolean;
|
|
33
|
+
children?: ContextMenuItemConfig[]; // 支持嵌套子菜单
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
interface UnifiedContextMenuProps {
|
|
37
|
+
items: ContextMenuItemConfig[];
|
|
38
|
+
children: JSX.Element; // 触发区域
|
|
39
|
+
class?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 递归渲染函数
|
|
43
|
+
const RenderMenuItems = (props: { items: ContextMenuItemConfig[] }) => {
|
|
44
|
+
return (
|
|
45
|
+
<For each={props.items}>
|
|
46
|
+
{(itemConfig) => (
|
|
47
|
+
<Show
|
|
48
|
+
when={!itemConfig.separator}
|
|
49
|
+
fallback={<KContextMenu.Separator class={separator()} />}
|
|
50
|
+
>
|
|
51
|
+
<Show
|
|
52
|
+
when={
|
|
53
|
+
itemConfig.children &&
|
|
54
|
+
itemConfig.children.length > 0
|
|
55
|
+
}
|
|
56
|
+
fallback={
|
|
57
|
+
<KContextMenu.Item
|
|
58
|
+
class={item()}
|
|
59
|
+
disabled={itemConfig.disabled}
|
|
60
|
+
onSelect={() => itemConfig.onClick?.()}
|
|
61
|
+
>
|
|
62
|
+
{itemConfig.label}
|
|
63
|
+
</KContextMenu.Item>
|
|
64
|
+
}
|
|
65
|
+
>
|
|
66
|
+
{/* 渲染子菜单 */}
|
|
67
|
+
<KContextMenu.Sub>
|
|
68
|
+
<KContextMenu.SubTrigger class={item()}>
|
|
69
|
+
{itemConfig.label}
|
|
70
|
+
<ChevronRight class={subIcon()} />
|
|
71
|
+
</KContextMenu.SubTrigger>
|
|
72
|
+
<KContextMenu.Portal>
|
|
73
|
+
<KContextMenu.SubContent class={content()}>
|
|
74
|
+
<RenderMenuItems
|
|
75
|
+
items={itemConfig.children!}
|
|
76
|
+
/>
|
|
77
|
+
</KContextMenu.SubContent>
|
|
78
|
+
</KContextMenu.Portal>
|
|
79
|
+
</KContextMenu.Sub>
|
|
80
|
+
</Show>
|
|
81
|
+
</Show>
|
|
82
|
+
)}
|
|
83
|
+
</For>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const ContextMenu = (props: UnifiedContextMenuProps) => {
|
|
88
|
+
const [local] = splitProps(props, ["items", "children", "class"]);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<KContextMenu>
|
|
92
|
+
<KContextMenu.Trigger class={local.class}>
|
|
93
|
+
{local.children}
|
|
94
|
+
</KContextMenu.Trigger>
|
|
95
|
+
<KContextMenu.Portal>
|
|
96
|
+
<KContextMenu.Content class={content()}>
|
|
97
|
+
<RenderMenuItems items={local.items} />
|
|
98
|
+
</KContextMenu.Content>
|
|
99
|
+
</KContextMenu.Portal>
|
|
100
|
+
</KContextMenu>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Dialog as KDialog } from "@kobalte/core/dialog";
|
|
2
|
+
import { splitProps, type JSX, Show } from "solid-js";
|
|
3
|
+
import { tv } from "tailwind-variants";
|
|
4
|
+
import { X } from "lucide-solid";
|
|
5
|
+
|
|
6
|
+
//TODO 修改footer,可自定义或是自带,方法传入等等
|
|
7
|
+
// FIXME title 和close icon 平行。
|
|
8
|
+
|
|
9
|
+
const dialogStyles = tv(
|
|
10
|
+
{
|
|
11
|
+
slots: {
|
|
12
|
+
overlay: [
|
|
13
|
+
"fixed inset-0 z-50 backdrop-blur-sm animate-in duration-200",
|
|
14
|
+
"data-[expanded]:animate-in data-[closed]:animate-out",
|
|
15
|
+
],
|
|
16
|
+
content: [
|
|
17
|
+
[
|
|
18
|
+
"fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-base bg-app text-main p-4 shadow-lg rounded-xl",
|
|
19
|
+
"data-[expanded]:animate-in data-[closed]:animate-out",
|
|
20
|
+
],
|
|
21
|
+
],
|
|
22
|
+
title: "text-lg font-semibold leading-none text-zinc-950 dark:text-zinc-50",
|
|
23
|
+
description: "text-sm text-zinc-500 dark:text-zinc-400 mt-2",
|
|
24
|
+
closeButton:
|
|
25
|
+
"rounded-sm opacity-70 transition-opacity hover:opacity-100 dark:text-zinc-400"
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
twMerge: true,
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const { overlay, content, title, description, closeButton } =
|
|
34
|
+
dialogStyles();
|
|
35
|
+
|
|
36
|
+
interface DialogProps {
|
|
37
|
+
// 状态控制
|
|
38
|
+
open?: boolean;
|
|
39
|
+
onOpenChange?: (open: boolean) => void;
|
|
40
|
+
|
|
41
|
+
// 触发器
|
|
42
|
+
trigger?: JSX.Element;
|
|
43
|
+
|
|
44
|
+
// 内容配置
|
|
45
|
+
title: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
children: JSX.Element; // 弹窗主体
|
|
48
|
+
|
|
49
|
+
// 底部按钮配置
|
|
50
|
+
footer?: JSX.Element;
|
|
51
|
+
|
|
52
|
+
// 样式
|
|
53
|
+
class?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const Dialog = (props: DialogProps) => {
|
|
57
|
+
const [local] = splitProps(props, [
|
|
58
|
+
"trigger",
|
|
59
|
+
"title",
|
|
60
|
+
"description",
|
|
61
|
+
"footer",
|
|
62
|
+
"children",
|
|
63
|
+
"class",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<KDialog open={props.open} onOpenChange={props.onOpenChange}>
|
|
68
|
+
<Show when={local.trigger}>
|
|
69
|
+
<KDialog.Trigger class="inline-block">
|
|
70
|
+
{local.trigger}
|
|
71
|
+
</KDialog.Trigger>
|
|
72
|
+
</Show>
|
|
73
|
+
|
|
74
|
+
<KDialog.Portal>
|
|
75
|
+
<KDialog.Overlay class={overlay()} />
|
|
76
|
+
<KDialog.Content
|
|
77
|
+
class={content({ class: local.class })}
|
|
78
|
+
style={{ "pointer-events": "auto" }}
|
|
79
|
+
>
|
|
80
|
+
<div class="flex justify-between items-center">
|
|
81
|
+
<KDialog.Title class={title()}>
|
|
82
|
+
{local.title}
|
|
83
|
+
</KDialog.Title>
|
|
84
|
+
<KDialog.CloseButton class={closeButton()}>
|
|
85
|
+
<X size={18} />
|
|
86
|
+
<span class="sr-only">关闭</span>
|
|
87
|
+
</KDialog.CloseButton>
|
|
88
|
+
</div>
|
|
89
|
+
<Show when={local.description}>
|
|
90
|
+
<KDialog.Description class={description()}>
|
|
91
|
+
{local.description}
|
|
92
|
+
</KDialog.Description>
|
|
93
|
+
</Show>
|
|
94
|
+
|
|
95
|
+
<div class="mt-6">{local.children}</div>
|
|
96
|
+
|
|
97
|
+
</KDialog.Content>
|
|
98
|
+
</KDialog.Portal>
|
|
99
|
+
</KDialog>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { DropdownMenu as KDropdownMenu } from "@kobalte/core/dropdown-menu";
|
|
2
|
+
import { splitProps, type JSX, For, Show } from "solid-js";
|
|
3
|
+
import { tv } from "tailwind-variants";
|
|
4
|
+
import { ChevronRight } from "lucide-solid";
|
|
5
|
+
|
|
6
|
+
// TODO Dropdown Menu 样式
|
|
7
|
+
|
|
8
|
+
const menuStyles = tv(
|
|
9
|
+
{
|
|
10
|
+
slots: {
|
|
11
|
+
trigger: "inline-block cursor-pointer",
|
|
12
|
+
content: [
|
|
13
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-light bg-white p-1 text-zinc-950 shadow-md animate-in zoom-in-95 dark:bg-zinc-950 dark:text-zinc-50",
|
|
14
|
+
"data-[expanded]:animate-in data-[closed]:animate-out",
|
|
15
|
+
],
|
|
16
|
+
item: "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[highlighted]:bg-zinc-100 data-[highlighted]:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:data-[highlighted]:bg-zinc-800 dark:data-[highlighted]:text-zinc-50",
|
|
17
|
+
separator: "-mx-1 my-1 h-px border-light",
|
|
18
|
+
subIcon: "ml-auto h-4 w-4",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
twMerge: true,
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const { content, item, separator, subIcon, trigger } = menuStyles();
|
|
27
|
+
|
|
28
|
+
// 定义菜单项配置类型
|
|
29
|
+
export type DropdownItemConfig = {
|
|
30
|
+
label: string;
|
|
31
|
+
onClick?: () => void;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
separator?: boolean; // 是否作为分隔线
|
|
34
|
+
children?: DropdownItemConfig[]; // 子菜单
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
interface DropdownMenuProps {
|
|
38
|
+
trigger: JSX.Element;
|
|
39
|
+
items: DropdownItemConfig[];
|
|
40
|
+
placement?:
|
|
41
|
+
| "bottom"
|
|
42
|
+
| "bottom-start"
|
|
43
|
+
| "bottom-end"
|
|
44
|
+
| "top"
|
|
45
|
+
| "left"
|
|
46
|
+
| "right";
|
|
47
|
+
class?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 递归渲染函数:处理无限级嵌套
|
|
51
|
+
const RenderMenuItems = (props: { items: DropdownItemConfig[] }) => {
|
|
52
|
+
return (
|
|
53
|
+
<For each={props.items}>
|
|
54
|
+
{(config) => (
|
|
55
|
+
<Show
|
|
56
|
+
when={!config.separator}
|
|
57
|
+
fallback={<KDropdownMenu.Separator class={separator()} />}
|
|
58
|
+
>
|
|
59
|
+
<Show
|
|
60
|
+
when={config.children && config.children.length > 0}
|
|
61
|
+
fallback={
|
|
62
|
+
<KDropdownMenu.Item
|
|
63
|
+
class={item()}
|
|
64
|
+
disabled={config.disabled}
|
|
65
|
+
onSelect={() => config.onClick?.()}
|
|
66
|
+
>
|
|
67
|
+
{config.label}
|
|
68
|
+
</KDropdownMenu.Item>
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
{/* 子菜单渲染逻辑 */}
|
|
72
|
+
<KDropdownMenu.Sub>
|
|
73
|
+
<KDropdownMenu.SubTrigger class={item()}>
|
|
74
|
+
{config.label}
|
|
75
|
+
<ChevronRight class={subIcon()} />
|
|
76
|
+
</KDropdownMenu.SubTrigger>
|
|
77
|
+
<KDropdownMenu.Portal>
|
|
78
|
+
<KDropdownMenu.SubContent class={content()}>
|
|
79
|
+
<RenderMenuItems items={config.children!} />
|
|
80
|
+
</KDropdownMenu.SubContent>
|
|
81
|
+
</KDropdownMenu.Portal>
|
|
82
|
+
</KDropdownMenu.Sub>
|
|
83
|
+
</Show>
|
|
84
|
+
</Show>
|
|
85
|
+
)}
|
|
86
|
+
</For>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const DropdownMenu = (props: DropdownMenuProps) => {
|
|
91
|
+
const [local] = splitProps(props, [
|
|
92
|
+
"trigger",
|
|
93
|
+
"items",
|
|
94
|
+
"placement",
|
|
95
|
+
"class",
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<KDropdownMenu placement={local.placement ?? "bottom-start"}>
|
|
100
|
+
<KDropdownMenu.Trigger as="div" class={trigger()}>
|
|
101
|
+
{local.trigger}
|
|
102
|
+
</KDropdownMenu.Trigger>
|
|
103
|
+
|
|
104
|
+
<KDropdownMenu.Portal>
|
|
105
|
+
<KDropdownMenu.Content class={content({ class: local.class })}>
|
|
106
|
+
<RenderMenuItems items={local.items} />
|
|
107
|
+
</KDropdownMenu.Content>
|
|
108
|
+
</KDropdownMenu.Portal>
|
|
109
|
+
</KDropdownMenu>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { TextField as KTextField } from "@kobalte/core/text-field";
|
|
2
|
+
import { splitProps, type ComponentProps, Show, createSignal } from "solid-js";
|
|
3
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
4
|
+
import { CloudUpload } from "lucide-solid";
|
|
5
|
+
|
|
6
|
+
//TODO 样式修改,移除 UploadCloud这种已废弃的icon
|
|
7
|
+
|
|
8
|
+
const fileFieldStyles = tv(
|
|
9
|
+
{
|
|
10
|
+
slots: {
|
|
11
|
+
root: "flex flex-col gap-1.5 w-full antialiased",
|
|
12
|
+
label: "text-sm font-medium text-slate-700 dark:text-slate-300 ml-1",
|
|
13
|
+
dropzone: [
|
|
14
|
+
"relative flex flex-col items-center justify-center w-full min-h-[140px]",
|
|
15
|
+
"border-2 border-dashed rounded-xl transition-all cursor-pointer",
|
|
16
|
+
"bg-slate-50/50 hover:bg-slate-100 dark:bg-slate-900/10 dark:hover:bg-slate-900/20",
|
|
17
|
+
"focus-within:ring-2 focus-within:ring-blue-500/20 focus-within:border-blue-500",
|
|
18
|
+
],
|
|
19
|
+
icon: "w-10 h-10 mb-3 text-slate-400",
|
|
20
|
+
description: "text-xs text-slate-500 dark:text-slate-400 mt-1",
|
|
21
|
+
errorMessage: "text-xs text-red-500 font-medium ml-1 mt-1",
|
|
22
|
+
},
|
|
23
|
+
variants: {
|
|
24
|
+
validationState: {
|
|
25
|
+
valid: {},
|
|
26
|
+
invalid: {
|
|
27
|
+
dropzone:
|
|
28
|
+
"border-red-400 bg-red-50/30 dark:border-red-900/20",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
isDisabled: {
|
|
32
|
+
true: {
|
|
33
|
+
dropzone: "opacity-50 cursor-not-allowed grayscale",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
twMerge: true,
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
type FileFieldVariants = VariantProps<typeof fileFieldStyles>;
|
|
44
|
+
|
|
45
|
+
export interface FileFieldProps
|
|
46
|
+
extends
|
|
47
|
+
Omit<ComponentProps<typeof KTextField>, "value" | "onChange">,
|
|
48
|
+
FileFieldVariants {
|
|
49
|
+
label?: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
accept?: string;
|
|
52
|
+
multiple?: boolean;
|
|
53
|
+
onChange?: (files: File[]) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const FileField = (props: FileFieldProps) => {
|
|
57
|
+
const [local, variantProps, others] = splitProps(
|
|
58
|
+
props,
|
|
59
|
+
["label", "description", "class", "accept", "multiple", "onChange"],
|
|
60
|
+
["validationState", "isDisabled"],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const [files, setFiles] = createSignal<File[]>([]);
|
|
64
|
+
const styles = () =>
|
|
65
|
+
fileFieldStyles({
|
|
66
|
+
validationState: variantProps.validationState,
|
|
67
|
+
isDisabled: variantProps.isDisabled,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const onFileChange = (e: Event) => {
|
|
71
|
+
const target = e.target as HTMLInputElement;
|
|
72
|
+
if (target.files) {
|
|
73
|
+
const fileList = Array.from(target.files);
|
|
74
|
+
setFiles(fileList);
|
|
75
|
+
local.onChange?.(fileList);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<KTextField
|
|
81
|
+
class={styles().root({ class: local.class })}
|
|
82
|
+
validationState={variantProps.validationState}
|
|
83
|
+
disabled={variantProps.isDisabled}
|
|
84
|
+
{...others}
|
|
85
|
+
>
|
|
86
|
+
<Show when={local.label}>
|
|
87
|
+
<KTextField.Label class={styles().label()}>
|
|
88
|
+
{local.label}
|
|
89
|
+
</KTextField.Label>
|
|
90
|
+
</Show>
|
|
91
|
+
|
|
92
|
+
<label class={styles().dropzone()}>
|
|
93
|
+
<KTextField.Input
|
|
94
|
+
type="file"
|
|
95
|
+
class="sr-only"
|
|
96
|
+
accept={local.accept}
|
|
97
|
+
multiple={local.multiple}
|
|
98
|
+
onChange={onFileChange}
|
|
99
|
+
/>
|
|
100
|
+
<CloudUpload class={styles().icon()} />
|
|
101
|
+
<div class="text-sm font-medium text-slate-600 dark:text-slate-400">
|
|
102
|
+
{files().length > 0
|
|
103
|
+
? `已选择 ${files().length} 个文件`
|
|
104
|
+
: "点击或拖拽上传文件"}
|
|
105
|
+
</div>
|
|
106
|
+
<Show when={local.description}>
|
|
107
|
+
<p class={styles().description()}>{local.description}</p>
|
|
108
|
+
</Show>
|
|
109
|
+
</label>
|
|
110
|
+
|
|
111
|
+
<KTextField.ErrorMessage class={styles().errorMessage()} />
|
|
112
|
+
</KTextField>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { HoverCard as KHoverCard } from "@kobalte/core/hover-card";
|
|
2
|
+
import { splitProps, type ComponentProps, type JSX } from "solid-js";
|
|
3
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
4
|
+
|
|
5
|
+
const hoverCardStyles = tv(
|
|
6
|
+
{
|
|
7
|
+
slots: {
|
|
8
|
+
content: [
|
|
9
|
+
"z-50 w-64 rounded-xl border border-light bg-app p-4 shadow-lg outline-none",
|
|
10
|
+
"data-[expanded]:animate-in data-[closed]:animate-out",
|
|
11
|
+
],
|
|
12
|
+
arrow: "fill-white stroke-slate-200 dark:fill-slate-900 dark:stroke-slate-800",
|
|
13
|
+
},
|
|
14
|
+
variants: {
|
|
15
|
+
size: {
|
|
16
|
+
sm: { content: "w-48 p-3" },
|
|
17
|
+
md: { content: "w-64 p-4" },
|
|
18
|
+
lg: { content: "w-80 p-6" },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
size: "md",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
twMerge: true,
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
type HoverCardVariants = VariantProps<typeof hoverCardStyles>;
|
|
31
|
+
|
|
32
|
+
export interface HoverCardProps
|
|
33
|
+
extends ComponentProps<typeof KHoverCard>, HoverCardVariants {
|
|
34
|
+
trigger: JSX.Element;
|
|
35
|
+
showArrow?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const HoverCard = (props: HoverCardProps) => {
|
|
39
|
+
const [local, variantProps, others] = splitProps(
|
|
40
|
+
props,
|
|
41
|
+
["trigger", "children", "showArrow"],
|
|
42
|
+
["size"],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const styles = hoverCardStyles({ size: variantProps.size });
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<KHoverCard openDelay={200} closeDelay={300} {...others}>
|
|
49
|
+
<KHoverCard.Trigger>{local.trigger}</KHoverCard.Trigger>
|
|
50
|
+
|
|
51
|
+
<KHoverCard.Portal>
|
|
52
|
+
<KHoverCard.Content class={styles.content()}>
|
|
53
|
+
{local.showArrow && (
|
|
54
|
+
<KHoverCard.Arrow class={styles.arrow()} />
|
|
55
|
+
)}
|
|
56
|
+
{local.children}
|
|
57
|
+
</KHoverCard.Content>
|
|
58
|
+
</KHoverCard.Portal>
|
|
59
|
+
</KHoverCard>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Image as KImage } from "@kobalte/core/image";
|
|
2
|
+
import { splitProps, type ComponentProps } from "solid-js";
|
|
3
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
4
|
+
|
|
5
|
+
const imageStyles = tv(
|
|
6
|
+
{
|
|
7
|
+
slots: {
|
|
8
|
+
root: "relative flex items-center h-full w-full shrink-0 overflow-hidden",
|
|
9
|
+
img: "h-full w-full aspect-square object-cover",
|
|
10
|
+
fallback:
|
|
11
|
+
"flex h-full w-full items-center justify-center bg-slate-100 dark:bg-slate-800 text-slate-400",
|
|
12
|
+
},
|
|
13
|
+
variants: {
|
|
14
|
+
radius: {
|
|
15
|
+
none: { root: "rounded-none" },
|
|
16
|
+
sm: { root: "rounded-sm" },
|
|
17
|
+
md: { root: "rounded-md" },
|
|
18
|
+
lg: { root: "rounded-lg" },
|
|
19
|
+
full: { root: "rounded-full" },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
radius: "none",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
twMerge: true,
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
type ImageVariants = VariantProps<typeof imageStyles>;
|
|
32
|
+
|
|
33
|
+
export interface ImageProps
|
|
34
|
+
extends ComponentProps<typeof KImage>,
|
|
35
|
+
ImageVariants {
|
|
36
|
+
src?: string;
|
|
37
|
+
alt?: string;
|
|
38
|
+
fallback?: string | Array<any> | any; // 支持自定义 fallback 内容
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const Image = (props: ImageProps) => {
|
|
42
|
+
const [local, variantProps, others] = splitProps(
|
|
43
|
+
props,
|
|
44
|
+
["src", "alt", "fallback", "class"],
|
|
45
|
+
["radius"]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const styles = imageStyles({ radius: variantProps.radius });
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<KImage class={styles.root({ class: local.class })} {...others}>
|
|
52
|
+
<KImage.Img src={local.src} alt={local.alt} class={styles.img()} />
|
|
53
|
+
<KImage.Fallback class={styles.fallback()}>
|
|
54
|
+
{local.fallback ||
|
|
55
|
+
(local.alt ? local.alt.slice(0, 2).toUpperCase() : "IMG")}
|
|
56
|
+
</KImage.Fallback>
|
|
57
|
+
</KImage>
|
|
58
|
+
);
|
|
59
|
+
};
|