solid-element-ui 0.2.3 → 0.2.5
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/index.css +1 -1
- package/dist/index.js +572 -579
- package/dist/src/alert-dialog/alert-dialog.d.ts +2 -1
- package/package.json +5 -5
- package/src/accordion/accordion.tsx +80 -0
- package/src/alert/alert.tsx +86 -0
- package/src/alert-dialog/alert-dialog.tsx +98 -0
- package/src/badge/badge.tsx +52 -0
- package/src/breadcrumbs/breadcrumbs.tsx +69 -0
- package/src/button/button.tsx +216 -0
- package/src/checkbox/checkbox.tsx +64 -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 +102 -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 +62 -0
- package/src/image/image.tsx +59 -0
- package/src/index.tsx +91 -0
- package/src/link/link.tsx +64 -0
- package/src/menubar/menubar.tsx +81 -0
- package/src/meter/meter.tsx +89 -0
- package/src/navigation-menu/navigation-menu.tsx +90 -0
- package/src/number-field/number-field.tsx +80 -0
- package/src/pagination/pagination.tsx +68 -0
- package/src/popover/popover.tsx +59 -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 +164 -0
- package/src/separator/separator.tsx +62 -0
- package/src/skeleton/skeleton.tsx +73 -0
- package/src/slider/slider.tsx +91 -0
- package/src/style/index.css +150 -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 +128 -0
- package/src/toggle-button/toggle-button.tsx +68 -0
- package/src/toggle-group/toggle-group.tsx +86 -0
- package/src/tooltip/tooltip.tsx +78 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/* --- 1. 先定义 CSS 变量 --- */
|
|
4
|
+
@layer theme {
|
|
5
|
+
:root {
|
|
6
|
+
--primary: var(--color-blue-600);
|
|
7
|
+
--success: var(--color-green-600);
|
|
8
|
+
--warning: var(--color-orange-600);
|
|
9
|
+
--danger: var(--color-red-600);
|
|
10
|
+
|
|
11
|
+
--app-bg: #ffffff;
|
|
12
|
+
--app-text-reversal: #ffffff;
|
|
13
|
+
--app-text-main: var(--color-zinc-900);
|
|
14
|
+
--app-text-muted: var(--color-zinc-500);
|
|
15
|
+
--app-border: var(--color-zinc-500);
|
|
16
|
+
--app-ring: var(--color-zinc-400);
|
|
17
|
+
--container-header: var(--color-zinc-100);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
[data-theme="dark"] {
|
|
21
|
+
--app-text-reversal: #000;
|
|
22
|
+
--app-bg: var(--color-zinc-950);
|
|
23
|
+
--app-text-main: var(--color-zinc-50);
|
|
24
|
+
--app-text-muted: var(--color-zinc-400);
|
|
25
|
+
--app-ring: var(--color-zinc-600);
|
|
26
|
+
--container-header: var(--color-zinc-900);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[data-theme="coffee"] {
|
|
30
|
+
--app-bg: #2c2420;
|
|
31
|
+
--app-text-main: #f3e5d8;
|
|
32
|
+
--app-ring: #8b5e3c;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@keyframes accordion-down {
|
|
36
|
+
from {
|
|
37
|
+
height: 0;
|
|
38
|
+
}
|
|
39
|
+
to {
|
|
40
|
+
height: var(--kb-accordion-content-height);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@keyframes accordion-up {
|
|
45
|
+
from {
|
|
46
|
+
height: var(--kb-accordion-content-height);
|
|
47
|
+
}
|
|
48
|
+
to {
|
|
49
|
+
height: 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@keyframes fade-in {
|
|
54
|
+
from {
|
|
55
|
+
opacity: 0;
|
|
56
|
+
}
|
|
57
|
+
to {
|
|
58
|
+
opacity: 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@keyframes fade-out {
|
|
63
|
+
from {
|
|
64
|
+
opacity: 1;
|
|
65
|
+
}
|
|
66
|
+
to {
|
|
67
|
+
opacity: 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@keyframes collapsible-down {
|
|
72
|
+
from {
|
|
73
|
+
height: 0;
|
|
74
|
+
}
|
|
75
|
+
to {
|
|
76
|
+
height: var(--kb-collapsible-content-height);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@keyframes collapsible-up {
|
|
81
|
+
from {
|
|
82
|
+
height: var(--kb-collapsible-content-height);
|
|
83
|
+
}
|
|
84
|
+
to {
|
|
85
|
+
height: 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@keyframes slide-in {
|
|
90
|
+
from {
|
|
91
|
+
transform: translateX(calc(100% + var(--viewport-padding)));
|
|
92
|
+
}
|
|
93
|
+
to {
|
|
94
|
+
transform: translateX(0);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@keyframes hide {
|
|
99
|
+
from {
|
|
100
|
+
opacity: 1;
|
|
101
|
+
}
|
|
102
|
+
to {
|
|
103
|
+
opacity: 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@keyframes swipe-out {
|
|
108
|
+
from {
|
|
109
|
+
transform: translateX(var(--kb-toast-swipe-end-x));
|
|
110
|
+
}
|
|
111
|
+
to {
|
|
112
|
+
transform: translateX(calc(100% + var(--viewport-padding)));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* --- 2. 注册到 Tailwind 主题系统 --- */
|
|
118
|
+
@theme {
|
|
119
|
+
--color-primary: var(--primary);
|
|
120
|
+
--color-success: var(--success);
|
|
121
|
+
--color-warning: var(--warning);
|
|
122
|
+
--color-danger: var(--danger);
|
|
123
|
+
--color-reversal: var(--app-text-reversal);
|
|
124
|
+
--color-header: var(--container-header);
|
|
125
|
+
--color-app: var(--app-bg);
|
|
126
|
+
--color-main: var(--app-text-main);
|
|
127
|
+
--color-muted: var(--app-text-muted);
|
|
128
|
+
--color-base: var(--app-border);
|
|
129
|
+
--color-accent-ring: var(--app-ring);
|
|
130
|
+
--radius-app: 0.75rem;
|
|
131
|
+
|
|
132
|
+
/* 注册动画到主题 */
|
|
133
|
+
--animate-accordion-down: accordion-down 0.2s ease-out;
|
|
134
|
+
--animate-accordion-up: accordion-up 0.2s ease-out;
|
|
135
|
+
|
|
136
|
+
--animate-in: fade-in 0.2s ease-in;
|
|
137
|
+
--animate-out: fade-out 0.2s ease-out forwards;
|
|
138
|
+
|
|
139
|
+
--animate-collapsible-down: collapsible-down 0.2s ease-out;
|
|
140
|
+
--animate-collapsible-up: collapsible-up 0.2s ease-out;
|
|
141
|
+
|
|
142
|
+
--animate-slide-in: slide-in 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
143
|
+
--animate-hide: hide 100ms ease-in;
|
|
144
|
+
--animate-swipe-out: swipe-out 100ms ease-out;
|
|
145
|
+
|
|
146
|
+
--viewport-padding: 16px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* --- 3. 暗色主题变体 --- */
|
|
150
|
+
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Switch as KSwitch } from "@kobalte/core/switch";
|
|
2
|
+
import { splitProps, type ComponentProps, Show } from "solid-js";
|
|
3
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
4
|
+
|
|
5
|
+
const switchStyles = tv(
|
|
6
|
+
{
|
|
7
|
+
slots: {
|
|
8
|
+
root: "inline-flex items-center gap-2 group",
|
|
9
|
+
control: [
|
|
10
|
+
"inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors",
|
|
11
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
|
|
12
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
13
|
+
"bg-slate-200 dark:bg-slate-800 data-[checked]:bg-blue-600 dark:data-[checked]:bg-blue-500",
|
|
14
|
+
],
|
|
15
|
+
thumb: [
|
|
16
|
+
"pointer-events-none block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform",
|
|
17
|
+
"data-[checked]:translate-x-5 translate-x-0",
|
|
18
|
+
],
|
|
19
|
+
label: "text-sm font-medium leading-none group-data-[disabled]:opacity-70",
|
|
20
|
+
description: "text-xs text-slate-500 dark:text-slate-400",
|
|
21
|
+
},
|
|
22
|
+
variants: {
|
|
23
|
+
size: {
|
|
24
|
+
sm: {
|
|
25
|
+
control: "h-5 w-9",
|
|
26
|
+
thumb: "h-4 w-4 data-[checked]:translate-x-4",
|
|
27
|
+
},
|
|
28
|
+
md: {
|
|
29
|
+
control: "h-6 w-11",
|
|
30
|
+
thumb: "h-5 w-5 data-[checked]:translate-x-5",
|
|
31
|
+
},
|
|
32
|
+
lg: {
|
|
33
|
+
control: "h-7 w-13",
|
|
34
|
+
thumb: "h-6 w-6 data-[checked]:translate-x-6",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
variant: {
|
|
38
|
+
primary: {
|
|
39
|
+
control:
|
|
40
|
+
"data-[checked]:bg-blue-600 dark:data-[checked]:bg-blue-500",
|
|
41
|
+
},
|
|
42
|
+
success: {
|
|
43
|
+
control:
|
|
44
|
+
"data-[checked]:bg-emerald-600 dark:data-[checked]:bg-emerald-500",
|
|
45
|
+
},
|
|
46
|
+
danger: {
|
|
47
|
+
control:
|
|
48
|
+
"data-[checked]:bg-red-600 dark:data-[checked]:bg-red-500",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
defaultVariants: {
|
|
53
|
+
size: "md",
|
|
54
|
+
variant: "primary",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
twMerge: true,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
type SwitchVariants = VariantProps<typeof switchStyles>;
|
|
63
|
+
|
|
64
|
+
export interface SwitchProps
|
|
65
|
+
extends Omit<ComponentProps<typeof KSwitch>, "class">,
|
|
66
|
+
SwitchVariants {
|
|
67
|
+
label?: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
class?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const Switch = (props: SwitchProps) => {
|
|
73
|
+
const [local, variantProps, others] = splitProps(
|
|
74
|
+
props,
|
|
75
|
+
["label", "description", "class"],
|
|
76
|
+
["size", "variant"]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const styles = switchStyles(variantProps);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<KSwitch class={styles.root({ class: local.class })} {...others}>
|
|
83
|
+
<KSwitch.Input />
|
|
84
|
+
<KSwitch.Control class={styles.control()}>
|
|
85
|
+
<KSwitch.Thumb class={styles.thumb()} />
|
|
86
|
+
</KSwitch.Control>
|
|
87
|
+
|
|
88
|
+
<Show when={local.label || local.description}>
|
|
89
|
+
<div class="flex flex-col gap-0.5">
|
|
90
|
+
<Show when={local.label}>
|
|
91
|
+
<KSwitch.Label class={styles.label()}>
|
|
92
|
+
{local.label}
|
|
93
|
+
</KSwitch.Label>
|
|
94
|
+
</Show>
|
|
95
|
+
<Show when={local.description}>
|
|
96
|
+
<KSwitch.Description class={styles.description()}>
|
|
97
|
+
{local.description}
|
|
98
|
+
</KSwitch.Description>
|
|
99
|
+
</Show>
|
|
100
|
+
</div>
|
|
101
|
+
</Show>
|
|
102
|
+
</KSwitch>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Tabs as KTabs } from "@kobalte/core/tabs";
|
|
2
|
+
import { splitProps, type JSX, For } from "solid-js";
|
|
3
|
+
import { tv } from "tailwind-variants";
|
|
4
|
+
|
|
5
|
+
const tabsStyles = tv(
|
|
6
|
+
{
|
|
7
|
+
slots: {
|
|
8
|
+
root: "flex flex-col w-full",
|
|
9
|
+
list: "relative flex items-center border-b border-zinc-200 dark:border-zinc-800",
|
|
10
|
+
trigger: [
|
|
11
|
+
"relative flex h-9 items-center justify-center px-4 text-sm font-medium transition-colors outline-none select-none cursor-pointer",
|
|
12
|
+
"text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200",
|
|
13
|
+
"data-[selected]:text-zinc-950 dark:data-[selected]:text-zinc-50",
|
|
14
|
+
],
|
|
15
|
+
indicator:
|
|
16
|
+
"absolute bottom-[-1px] h-0.5 bg-zinc-950 dark:bg-zinc-50 transition-all duration-200",
|
|
17
|
+
content:
|
|
18
|
+
"mt-4 text-sm text-zinc-600 dark:text-zinc-400 focus-visible:outline-none",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
twMerge: true,
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const { root, list, trigger, indicator, content } = tabsStyles();
|
|
27
|
+
|
|
28
|
+
export type TabItem = {
|
|
29
|
+
value: string;
|
|
30
|
+
label: string | JSX.Element;
|
|
31
|
+
content: JSX.Element;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
interface TabsProps {
|
|
36
|
+
items: TabItem[];
|
|
37
|
+
defaultValue?: string;
|
|
38
|
+
value?: string;
|
|
39
|
+
onValueChange?: (value: string) => void;
|
|
40
|
+
orientation?: "horizontal" | "vertical";
|
|
41
|
+
class?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const Tabs = (props: TabsProps) => {
|
|
45
|
+
const [local, others] = splitProps(props, ["items", "class"]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<KTabs class={root({ class: local.class })} {...others}>
|
|
49
|
+
<KTabs.List class={list()}>
|
|
50
|
+
<For each={local.items}>
|
|
51
|
+
{(item) => (
|
|
52
|
+
<KTabs.Trigger
|
|
53
|
+
class={trigger()}
|
|
54
|
+
value={item.value}
|
|
55
|
+
disabled={item.disabled}
|
|
56
|
+
>
|
|
57
|
+
{item.label}
|
|
58
|
+
</KTabs.Trigger>
|
|
59
|
+
)}
|
|
60
|
+
</For>
|
|
61
|
+
<KTabs.Indicator class={indicator()} />
|
|
62
|
+
</KTabs.List>
|
|
63
|
+
|
|
64
|
+
<For each={local.items}>
|
|
65
|
+
{(item) => (
|
|
66
|
+
<KTabs.Content class={content()} value={item.value}>
|
|
67
|
+
{item.content}
|
|
68
|
+
</KTabs.Content>
|
|
69
|
+
)}
|
|
70
|
+
</For>
|
|
71
|
+
</KTabs>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { TextField as KTextField } from "@kobalte/core/text-field";
|
|
2
|
+
import { splitProps, type ComponentProps, Show } from "solid-js";
|
|
3
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
4
|
+
|
|
5
|
+
const textFieldStyles = tv(
|
|
6
|
+
{
|
|
7
|
+
slots: {
|
|
8
|
+
root: "flex flex-col gap-1.5 w-full",
|
|
9
|
+
label: "text-sm font-medium text-slate-700 dark:text-slate-300 peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
10
|
+
input: [
|
|
11
|
+
"flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm transition-shadow",
|
|
12
|
+
"ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium",
|
|
13
|
+
"placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500",
|
|
14
|
+
"disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:bg-slate-950",
|
|
15
|
+
"data-[invalid]:border-red-500 data-[invalid]:focus-visible:ring-red-500",
|
|
16
|
+
],
|
|
17
|
+
description: "text-xs text-slate-500 dark:text-slate-400",
|
|
18
|
+
errorMessage:
|
|
19
|
+
"text-xs text-red-500 animate-in fade-in-50 slide-in-from-top-1",
|
|
20
|
+
},
|
|
21
|
+
variants: {
|
|
22
|
+
size: {
|
|
23
|
+
sm: { input: "h-8 px-2 text-xs" },
|
|
24
|
+
md: { input: "h-10 px-3 text-sm" },
|
|
25
|
+
lg: { input: "h-12 px-4 text-base" },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
size: "md",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
twMerge: true,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
type TextFieldVariants = VariantProps<typeof textFieldStyles>;
|
|
38
|
+
|
|
39
|
+
export interface TextFieldProps
|
|
40
|
+
extends Omit<ComponentProps<typeof KTextField>, "class">,
|
|
41
|
+
TextFieldVariants {
|
|
42
|
+
label?: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
errorMessage?: string;
|
|
45
|
+
placeholder?: string;
|
|
46
|
+
type?: string;
|
|
47
|
+
class?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const TextField = (props: TextFieldProps) => {
|
|
51
|
+
const [local, variantProps, others] = splitProps(
|
|
52
|
+
props,
|
|
53
|
+
[
|
|
54
|
+
"label",
|
|
55
|
+
"description",
|
|
56
|
+
"errorMessage",
|
|
57
|
+
"placeholder",
|
|
58
|
+
"type",
|
|
59
|
+
"class",
|
|
60
|
+
],
|
|
61
|
+
["size"]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const styles = textFieldStyles(variantProps);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<KTextField
|
|
68
|
+
class={styles.root({ class: local.class })}
|
|
69
|
+
validationState={local.errorMessage ? "invalid" : "valid"}
|
|
70
|
+
{...others}
|
|
71
|
+
>
|
|
72
|
+
<Show when={local.label}>
|
|
73
|
+
<KTextField.Label class={styles.label()}>
|
|
74
|
+
{local.label}
|
|
75
|
+
</KTextField.Label>
|
|
76
|
+
</Show>
|
|
77
|
+
|
|
78
|
+
<KTextField.Input
|
|
79
|
+
class={styles.input()}
|
|
80
|
+
type={local.type}
|
|
81
|
+
placeholder={local.placeholder}
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
<Show when={local.description}>
|
|
85
|
+
<KTextField.Description class={styles.description()}>
|
|
86
|
+
{local.description}
|
|
87
|
+
</KTextField.Description>
|
|
88
|
+
</Show>
|
|
89
|
+
|
|
90
|
+
<Show when={local.errorMessage}>
|
|
91
|
+
<KTextField.ErrorMessage class={styles.errorMessage()}>
|
|
92
|
+
{local.errorMessage}
|
|
93
|
+
</KTextField.ErrorMessage>
|
|
94
|
+
</Show>
|
|
95
|
+
</KTextField>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// import { TimeField as KTimeField } from "@kobalte/core/time-field";
|
|
2
|
+
// import { splitProps, For, type ComponentProps, Show } from "solid-js";
|
|
3
|
+
// import { tv, type VariantProps } from "tailwind-variants";
|
|
4
|
+
|
|
5
|
+
// const timeFieldStyles = tv({
|
|
6
|
+
// slots: {
|
|
7
|
+
// root: "flex flex-col gap-1.5 w-full",
|
|
8
|
+
// label: "text-sm font-medium text-slate-700 dark:text-slate-300 peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
9
|
+
// control: [
|
|
10
|
+
// "flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm transition-shadow",
|
|
11
|
+
// "focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2",
|
|
12
|
+
// "disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:bg-slate-950",
|
|
13
|
+
// "data-[invalid]:border-red-500 data-[invalid]:focus-within:ring-red-500",
|
|
14
|
+
// ],
|
|
15
|
+
// segment: [
|
|
16
|
+
// "inline rounded-sm px-0.5 tabular-nums outline-none transition-colors",
|
|
17
|
+
// "focus:bg-blue-600 focus:text-white dark:focus:bg-blue-500",
|
|
18
|
+
// "data-[placeholder]:text-slate-400 data-[type=literal]:px-0",
|
|
19
|
+
// ],
|
|
20
|
+
// description: "text-xs text-slate-500 dark:text-slate-400",
|
|
21
|
+
// errorMessage: "text-xs text-red-500",
|
|
22
|
+
// },
|
|
23
|
+
// variants: {
|
|
24
|
+
// size: {
|
|
25
|
+
// sm: { control: "h-8 px-2 text-xs" },
|
|
26
|
+
// md: { control: "h-10 px-3 text-sm" },
|
|
27
|
+
// lg: { control: "h-12 px-4 text-base" },
|
|
28
|
+
// },
|
|
29
|
+
// },
|
|
30
|
+
// defaultVariants: {
|
|
31
|
+
// size: "md",
|
|
32
|
+
// },
|
|
33
|
+
// });
|
|
34
|
+
|
|
35
|
+
// type TimeFieldVariants = VariantProps<typeof timeFieldStyles>;
|
|
36
|
+
|
|
37
|
+
// export interface TimeFieldProps
|
|
38
|
+
// extends Omit<ComponentProps<typeof KTimeField>, "class">,
|
|
39
|
+
// TimeFieldVariants {
|
|
40
|
+
// label?: string;
|
|
41
|
+
// description?: string;
|
|
42
|
+
// errorMessage?: string;
|
|
43
|
+
// class?: string;
|
|
44
|
+
// }
|
|
45
|
+
|
|
46
|
+
// export const TimeField = (props: TimeFieldProps) => {
|
|
47
|
+
// const [local, variantProps, others] = splitProps(
|
|
48
|
+
// props,
|
|
49
|
+
// ["label", "description", "errorMessage", "class"],
|
|
50
|
+
// ["size"]
|
|
51
|
+
// );
|
|
52
|
+
|
|
53
|
+
// const styles = timeFieldStyles(variantProps);
|
|
54
|
+
|
|
55
|
+
// return (
|
|
56
|
+
// <KTimeField
|
|
57
|
+
// class={styles.root({ class: local.class })}
|
|
58
|
+
// validationState={local.errorMessage ? "invalid" : "valid"}
|
|
59
|
+
// {...others}
|
|
60
|
+
// >
|
|
61
|
+
// <Show when={local.label}>
|
|
62
|
+
// <KTimeField.Label class={styles.label()}>
|
|
63
|
+
// {local.label}
|
|
64
|
+
// </KTimeField.Label>
|
|
65
|
+
// </Show>
|
|
66
|
+
|
|
67
|
+
// <KTimeField.Input class={styles.control()}>
|
|
68
|
+
// <For
|
|
69
|
+
// each={
|
|
70
|
+
// others.value
|
|
71
|
+
// ? []
|
|
72
|
+
// : [1] /* 仅作为占位,Kobalte 内部会自动处理内容 */
|
|
73
|
+
// }
|
|
74
|
+
// >
|
|
75
|
+
// {() => <KTimeField.Segment class={styles.segment()} />}
|
|
76
|
+
// </For>
|
|
77
|
+
// {/* 注意:通常情况下,直接在 Input 内部放置 Segment 映射即可 */}
|
|
78
|
+
// {(state) => (
|
|
79
|
+
// <For each={state.segments()}>
|
|
80
|
+
// {(segment) => (
|
|
81
|
+
// <KTimeField.Segment
|
|
82
|
+
// segment={segment}
|
|
83
|
+
// class={styles.segment()}
|
|
84
|
+
// />
|
|
85
|
+
// )}
|
|
86
|
+
// </For>
|
|
87
|
+
// )}
|
|
88
|
+
// </KTimeField.Input>
|
|
89
|
+
|
|
90
|
+
// <Show when={local.description}>
|
|
91
|
+
// <KTimeField.Description class={styles.description()}>
|
|
92
|
+
// {local.description}
|
|
93
|
+
// </KTimeField.Description>
|
|
94
|
+
// </Show>
|
|
95
|
+
|
|
96
|
+
// <Show when={local.errorMessage}>
|
|
97
|
+
// <KTimeField.ErrorMessage class={styles.errorMessage()}>
|
|
98
|
+
// {local.errorMessage}
|
|
99
|
+
// </KTimeField.ErrorMessage>
|
|
100
|
+
// </Show>
|
|
101
|
+
// </KTimeField>
|
|
102
|
+
// );
|
|
103
|
+
// };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Toast as KToast, toaster } from "@kobalte/core/toast";
|
|
2
|
+
import {
|
|
3
|
+
splitProps,
|
|
4
|
+
type ComponentProps,
|
|
5
|
+
Show,
|
|
6
|
+
type ParentProps,
|
|
7
|
+
} from "solid-js";
|
|
8
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
9
|
+
import { X, CircleCheck, CircleAlert, Info, TriangleAlert } from "lucide-solid";
|
|
10
|
+
|
|
11
|
+
const toastStyles = tv(
|
|
12
|
+
{
|
|
13
|
+
slots: {
|
|
14
|
+
root: [
|
|
15
|
+
"group relative flex w-[400px] items-start justify-between space-x-4 overflow-hidden rounded-md border p-4 pr-8 shadow-lg transition-all",
|
|
16
|
+
"data-[opened]:animate-slide-in",
|
|
17
|
+
"data-[closed]:animate-hide",
|
|
18
|
+
|
|
19
|
+
// 滑动手势处理
|
|
20
|
+
"data-[swipe=move]:translate-x-[--kb-toast-swipe-move-x]",
|
|
21
|
+
"data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-transform data-[swipe=cancel]:duration-200 data-[swipe=cancel]:ease-out",
|
|
22
|
+
"data-[swipe=end]:animate-swipe-out",
|
|
23
|
+
],
|
|
24
|
+
title: "text-sm font-semibold",
|
|
25
|
+
description: "text-xs opacity-90 leading-relaxed",
|
|
26
|
+
closeButton: [
|
|
27
|
+
"absolute right-2 top-2 rounded-md p-1 text-slate-500 opacity-0 transition-opacity hover:text-slate-900",
|
|
28
|
+
"focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 dark:text-slate-400 dark:hover:text-slate-50",
|
|
29
|
+
],
|
|
30
|
+
content: "flex flex-col gap-1 flex-1",
|
|
31
|
+
icon: "h-5 w-5 shrink-0 mt-0.5", // 稍微下移一点对齐文字
|
|
32
|
+
},
|
|
33
|
+
variants: {
|
|
34
|
+
variant: {
|
|
35
|
+
info: {
|
|
36
|
+
root: "bg-white border-slate-200 text-slate-950 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-50",
|
|
37
|
+
icon: "text-blue-500",
|
|
38
|
+
},
|
|
39
|
+
success: {
|
|
40
|
+
root: "bg-emerald-50 border-emerald-200 text-emerald-900 dark:bg-emerald-950 dark:border-emerald-900 dark:text-emerald-50",
|
|
41
|
+
icon: "text-emerald-500",
|
|
42
|
+
},
|
|
43
|
+
warning: {
|
|
44
|
+
root: "bg-amber-50 border-amber-200 text-amber-900 dark:bg-amber-950 dark:border-amber-900 dark:text-amber-50",
|
|
45
|
+
icon: "text-amber-500",
|
|
46
|
+
},
|
|
47
|
+
error: {
|
|
48
|
+
root: "bg-red-50 border-red-200 text-red-900 dark:bg-red-950 dark:border-red-900 dark:text-red-50",
|
|
49
|
+
icon: "text-red-500",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
defaultVariants: { variant: "info" },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
twMerge: true,
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
type ToastVariants = VariantProps<typeof toastStyles>;
|
|
61
|
+
|
|
62
|
+
export interface ToastProps
|
|
63
|
+
extends Omit<ComponentProps<typeof KToast>, "class">, ToastVariants {
|
|
64
|
+
title?: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
class?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const iconMap = {
|
|
70
|
+
info: Info,
|
|
71
|
+
success: CircleCheck,
|
|
72
|
+
warning: TriangleAlert,
|
|
73
|
+
error: CircleAlert,
|
|
74
|
+
} as const; // 使用 const 断言增强类型推导
|
|
75
|
+
|
|
76
|
+
// 优化:允许包裹 Children,这样在 App.tsx 顶层包裹即可
|
|
77
|
+
export const ToastProvider = (props: ParentProps) => {
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
{props.children}
|
|
81
|
+
<KToast.Region>
|
|
82
|
+
<KToast.List class="fixed bottom-4 right-4 z-100 flex flex-col gap-3 w-full max-w-100 outline-none" />
|
|
83
|
+
</KToast.Region>
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const Toast = (props: ToastProps) => {
|
|
89
|
+
const [local, variantProps, others] = splitProps(
|
|
90
|
+
props,
|
|
91
|
+
["title", "description", "class", "toastId"],
|
|
92
|
+
["variant"]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const { root, icon, content, title, description, closeButton } =
|
|
96
|
+
toastStyles(variantProps);
|
|
97
|
+
// 显式回退到 info,确保 Icon 组件始终存在
|
|
98
|
+
const Icon = iconMap[variantProps.variant ?? "info"];
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<KToast
|
|
102
|
+
toastId={local.toastId}
|
|
103
|
+
class={root({ class: local.class })}
|
|
104
|
+
{...others}
|
|
105
|
+
>
|
|
106
|
+
<Icon class={icon()} />
|
|
107
|
+
<div class={content()}>
|
|
108
|
+
<Show when={local.title}>
|
|
109
|
+
<KToast.Title class={title()}>
|
|
110
|
+
{local.title}
|
|
111
|
+
</KToast.Title>
|
|
112
|
+
</Show>
|
|
113
|
+
<Show when={local.description}>
|
|
114
|
+
<KToast.Description class={description()}>
|
|
115
|
+
{local.description}
|
|
116
|
+
</KToast.Description>
|
|
117
|
+
</Show>
|
|
118
|
+
</div>
|
|
119
|
+
<KToast.CloseButton class={closeButton()}>
|
|
120
|
+
<X size={16} />
|
|
121
|
+
</KToast.CloseButton>
|
|
122
|
+
</KToast>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const showToast = (props: Omit<ToastProps, "toastId">) => {
|
|
127
|
+
return toaster.show((data) => <Toast toastId={data.toastId} {...props} />);
|
|
128
|
+
};
|