@nikala-ui/core 0.9.11 → 0.10.0
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/package.json +1 -1
- package/registry/aspect-ratio.json +17 -0
- package/registry/button.json +4 -1
- package/registry/collapsible.json +18 -0
- package/registry/combobox.json +4 -1
- package/registry/command.json +3 -2
- package/registry/context-menu.json +24 -0
- package/registry/create-form.json +1 -1
- package/registry/dialog.json +4 -1
- package/registry/dropdown-menu.json +4 -1
- package/registry/empty.json +17 -0
- package/registry/field.json +20 -0
- package/registry/form-message.json +16 -0
- package/registry/form.json +17 -0
- package/registry/icon-button.json +20 -0
- package/registry/index.json +191 -1
- package/registry/number-input.json +24 -0
- package/registry/resizable.json +21 -0
- package/registry/scroll-area.json +21 -0
- package/registry/select.json +4 -1
- package/registry/sheet.json +4 -1
- package/registry/spinner.json +18 -0
- package/registry/status.json +18 -0
- package/registry/theme-manager.json +1 -1
- package/registry/toggle.json +18 -0
- package/src/registry/components/ui/aspect-ratio.tsx +31 -0
- package/src/registry/components/ui/button.tsx +18 -3
- package/src/registry/components/ui/collapsible.tsx +73 -0
- package/src/registry/components/ui/combobox.tsx +23 -84
- package/src/registry/components/ui/command.tsx +154 -66
- package/src/registry/components/ui/context-menu.tsx +192 -0
- package/src/registry/components/ui/dialog.tsx +39 -44
- package/src/registry/components/ui/dropdown-menu.tsx +40 -45
- package/src/registry/components/ui/empty.tsx +83 -0
- package/src/registry/components/ui/field.tsx +67 -0
- package/src/registry/components/ui/form-message.tsx +36 -0
- package/src/registry/components/ui/form.tsx +21 -0
- package/src/registry/components/ui/icon-button.tsx +34 -0
- package/src/registry/components/ui/number-input.tsx +150 -0
- package/src/registry/components/ui/resizable.tsx +193 -0
- package/src/registry/components/ui/scroll-area.tsx +218 -0
- package/src/registry/components/ui/select.tsx +22 -16
- package/src/registry/components/ui/sheet.tsx +62 -63
- package/src/registry/components/ui/spinner.tsx +41 -0
- package/src/registry/components/ui/status.tsx +119 -0
- package/src/registry/components/ui/theme-toggle.tsx +6 -4
- package/src/registry/components/ui/toggle.tsx +101 -0
- package/src/registry/metadata.ts +84 -2
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSignal,
|
|
3
|
+
createEffect,
|
|
4
|
+
onCleanup,
|
|
5
|
+
splitProps,
|
|
6
|
+
type Component,
|
|
7
|
+
type JSX,
|
|
8
|
+
type Accessor,
|
|
9
|
+
} from "solid-js";
|
|
10
|
+
import { createScrollPosition, createElementSize } from "@nikala-ui/hooks";
|
|
11
|
+
import { cn } from "@/lib/cn";
|
|
12
|
+
|
|
13
|
+
export interface ScrollAreaProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
orientation?: "vertical" | "horizontal" | "both";
|
|
15
|
+
scrollHideDelay?: number;
|
|
16
|
+
class?: string;
|
|
17
|
+
children?: JSX.Element;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ScrollArea: Component<ScrollAreaProps> = (props) => {
|
|
21
|
+
const [local, rest] = splitProps(props, [
|
|
22
|
+
"orientation",
|
|
23
|
+
"scrollHideDelay",
|
|
24
|
+
"class",
|
|
25
|
+
"children",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const orientation = () => local.orientation || "vertical";
|
|
29
|
+
let viewportRef: HTMLDivElement | undefined;
|
|
30
|
+
let verticalTrackRef: HTMLDivElement | undefined;
|
|
31
|
+
let horizontalTrackRef: HTMLDivElement | undefined;
|
|
32
|
+
|
|
33
|
+
const scrollPos = createScrollPosition({
|
|
34
|
+
target: () => viewportRef,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const viewportSize = createElementSize(() => viewportRef);
|
|
38
|
+
|
|
39
|
+
const [thumbHeight, setThumbHeight] = createSignal(0);
|
|
40
|
+
const [thumbTop, setThumbTop] = createSignal(0);
|
|
41
|
+
const [thumbWidth, setThumbWidth] = createSignal(0);
|
|
42
|
+
const [thumbLeft, setThumbLeft] = createSignal(0);
|
|
43
|
+
const [isDragging, setIsDragging] = createSignal(false);
|
|
44
|
+
|
|
45
|
+
const updateThumbMetrics = () => {
|
|
46
|
+
if (!viewportRef) return;
|
|
47
|
+
|
|
48
|
+
const scrollHeight = viewportRef.scrollHeight;
|
|
49
|
+
const clientHeight = viewportRef.clientHeight;
|
|
50
|
+
const scrollWidth = viewportRef.scrollWidth;
|
|
51
|
+
const clientWidth = viewportRef.clientWidth;
|
|
52
|
+
|
|
53
|
+
const trackHeight = verticalTrackRef ? verticalTrackRef.clientHeight - 4 : clientHeight - 4;
|
|
54
|
+
const trackWidth = horizontalTrackRef ? horizontalTrackRef.clientWidth - 4 : clientWidth - 4;
|
|
55
|
+
|
|
56
|
+
if (scrollHeight > clientHeight && clientHeight > 0) {
|
|
57
|
+
const vRatio = clientHeight / scrollHeight;
|
|
58
|
+
const calculatedHeight = Math.max(vRatio * trackHeight, 20);
|
|
59
|
+
const maxTop = trackHeight - calculatedHeight;
|
|
60
|
+
const topPct = scrollPos.y() / (scrollHeight - clientHeight);
|
|
61
|
+
setThumbHeight(calculatedHeight);
|
|
62
|
+
setThumbTop(topPct * maxTop);
|
|
63
|
+
} else {
|
|
64
|
+
setThumbHeight(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (scrollWidth > clientWidth && clientWidth > 0) {
|
|
68
|
+
const hRatio = clientWidth / scrollWidth;
|
|
69
|
+
const calculatedWidth = Math.max(hRatio * trackWidth, 20);
|
|
70
|
+
const maxLeft = trackWidth - calculatedWidth;
|
|
71
|
+
const leftPct = scrollPos.x() / (scrollWidth - clientWidth);
|
|
72
|
+
setThumbWidth(calculatedWidth);
|
|
73
|
+
setThumbLeft(leftPct * maxLeft);
|
|
74
|
+
} else {
|
|
75
|
+
setThumbWidth(0);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
createEffect(() => {
|
|
80
|
+
viewportSize.width();
|
|
81
|
+
viewportSize.height();
|
|
82
|
+
scrollPos.x();
|
|
83
|
+
scrollPos.y();
|
|
84
|
+
updateThumbMetrics();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const handleVerticalThumbPointerDown = (e: PointerEvent) => {
|
|
88
|
+
if (!viewportRef || !verticalTrackRef) return;
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
|
|
92
|
+
setIsDragging(true);
|
|
93
|
+
const startY = e.clientY;
|
|
94
|
+
const startScrollTop = viewportRef.scrollTop;
|
|
95
|
+
const scrollHeight = viewportRef.scrollHeight;
|
|
96
|
+
const clientHeight = viewportRef.clientHeight;
|
|
97
|
+
const trackHeight = verticalTrackRef.clientHeight - 4;
|
|
98
|
+
|
|
99
|
+
const maxScrollTop = scrollHeight - clientHeight;
|
|
100
|
+
const maxThumbTop = trackHeight - thumbHeight();
|
|
101
|
+
const ratio = maxThumbTop > 0 ? maxScrollTop / maxThumbTop : 0;
|
|
102
|
+
|
|
103
|
+
const onPointerMove = (moveEvent: PointerEvent) => {
|
|
104
|
+
const deltaY = moveEvent.clientY - startY;
|
|
105
|
+
viewportRef!.scrollTop = Math.max(0, Math.min(maxScrollTop, startScrollTop + deltaY * ratio));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const onPointerUp = () => {
|
|
109
|
+
setIsDragging(false);
|
|
110
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
111
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
115
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleHorizontalThumbPointerDown = (e: PointerEvent) => {
|
|
119
|
+
if (!viewportRef || !horizontalTrackRef) return;
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
|
|
123
|
+
setIsDragging(true);
|
|
124
|
+
const startX = e.clientX;
|
|
125
|
+
const startScrollLeft = viewportRef.scrollLeft;
|
|
126
|
+
const scrollWidth = viewportRef.scrollWidth;
|
|
127
|
+
const clientWidth = viewportRef.clientWidth;
|
|
128
|
+
const trackWidth = horizontalTrackRef.clientWidth - 4;
|
|
129
|
+
|
|
130
|
+
const maxScrollLeft = scrollWidth - clientWidth;
|
|
131
|
+
const maxThumbLeft = trackWidth - thumbWidth();
|
|
132
|
+
const ratio = maxThumbLeft > 0 ? maxScrollLeft / maxThumbLeft : 0;
|
|
133
|
+
|
|
134
|
+
const onPointerMove = (moveEvent: PointerEvent) => {
|
|
135
|
+
const deltaX = moveEvent.clientX - startX;
|
|
136
|
+
viewportRef!.scrollLeft = Math.max(0, Math.min(maxScrollLeft, startScrollLeft + deltaX * ratio));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const onPointerUp = () => {
|
|
140
|
+
setIsDragging(false);
|
|
141
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
142
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
146
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handleWheel = (e: WheelEvent) => {
|
|
150
|
+
if (orientation() === "horizontal" && viewportRef) {
|
|
151
|
+
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
viewportRef.scrollLeft += e.deltaY;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
class={cn("relative overflow-hidden group/scroll-area", local.class)}
|
|
161
|
+
onWheel={handleWheel}
|
|
162
|
+
{...rest}
|
|
163
|
+
>
|
|
164
|
+
<div
|
|
165
|
+
ref={viewportRef}
|
|
166
|
+
class="h-full w-full overflow-auto scrollbar-none rounded-[inherit]"
|
|
167
|
+
style={{
|
|
168
|
+
"scrollbar-width": "none",
|
|
169
|
+
"-ms-overflow-style": "none",
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{local.children}
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{(orientation() === "vertical" || orientation() === "both") && thumbHeight() > 0 && (
|
|
176
|
+
<div
|
|
177
|
+
ref={verticalTrackRef}
|
|
178
|
+
class={cn(
|
|
179
|
+
"absolute right-0 top-0 bottom-0 w-2.5 p-0.5 select-none transition-opacity duration-300 pointer-events-none",
|
|
180
|
+
scrollPos.isScrolling() || isDragging()
|
|
181
|
+
? "opacity-100"
|
|
182
|
+
: "opacity-0 group-hover/scroll-area:opacity-100"
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
<div
|
|
186
|
+
class="w-1.5 rounded-full bg-border hover:bg-muted-foreground/50 transition-colors cursor-pointer pointer-events-auto"
|
|
187
|
+
style={{
|
|
188
|
+
height: `${thumbHeight()}px`,
|
|
189
|
+
transform: `translateY(${thumbTop()}px)`,
|
|
190
|
+
}}
|
|
191
|
+
onPointerDown={handleVerticalThumbPointerDown}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{(orientation() === "horizontal" || orientation() === "both") && thumbWidth() > 0 && (
|
|
197
|
+
<div
|
|
198
|
+
ref={horizontalTrackRef}
|
|
199
|
+
class={cn(
|
|
200
|
+
"absolute bottom-0 left-0 right-0 h-2.5 p-0.5 select-none transition-opacity duration-300 pointer-events-none",
|
|
201
|
+
scrollPos.isScrolling() || isDragging()
|
|
202
|
+
? "opacity-100"
|
|
203
|
+
: "opacity-0 group-hover/scroll-area:opacity-100"
|
|
204
|
+
)}
|
|
205
|
+
>
|
|
206
|
+
<div
|
|
207
|
+
class="h-1.5 rounded-full bg-border hover:bg-muted-foreground/50 transition-colors cursor-pointer pointer-events-auto"
|
|
208
|
+
style={{
|
|
209
|
+
width: `${thumbWidth()}px`,
|
|
210
|
+
transform: `translateX(${thumbLeft()}px)`,
|
|
211
|
+
}}
|
|
212
|
+
onPointerDown={handleHorizontalThumbPointerDown}
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
@@ -1,23 +1,20 @@
|
|
|
1
|
-
import { splitProps, type JSX, type ValidComponent } from "solid-js";
|
|
1
|
+
import { splitProps, type Component, type JSX, type ValidComponent } from "solid-js";
|
|
2
2
|
import * as SelectPrimitive from "@kobalte/core/select";
|
|
3
3
|
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
|
4
4
|
import { createClickOutside } from "@nikala-ui/hooks";
|
|
5
|
+
import { ScrollArea } from "./scroll-area";
|
|
5
6
|
import { cn } from "@/lib/cn";
|
|
6
7
|
|
|
7
8
|
export type SelectRootProps<Option = any, OptGroup = any, T extends ValidComponent = "div"> =
|
|
8
|
-
SelectPrimitive.SelectRootProps<Option, OptGroup, T
|
|
9
|
-
class?: string;
|
|
10
|
-
};
|
|
9
|
+
SelectPrimitive.SelectRootProps<Option, OptGroup, T>;
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
|
-
* Root Select component built on
|
|
12
|
+
* Root Select component wrapper built on Kobalte primitives.
|
|
14
13
|
*/
|
|
15
14
|
export const Select = <Option = any, OptGroup = any, T extends ValidComponent = "div">(
|
|
16
|
-
props:
|
|
15
|
+
props: SelectRootProps<Option, OptGroup, T>
|
|
17
16
|
) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return <SelectPrimitive.Root class={cn("relative w-full", local.class)} {...(rest as any)} />;
|
|
17
|
+
return <SelectPrimitive.Root {...props} />;
|
|
21
18
|
};
|
|
22
19
|
|
|
23
20
|
export type SelectTriggerProps<T extends ValidComponent = "button"> =
|
|
@@ -109,12 +106,14 @@ export const SelectContent = <T extends ValidComponent = "div">(
|
|
|
109
106
|
if (typeof (props as any).ref === "function") (props as any).ref(el);
|
|
110
107
|
}}
|
|
111
108
|
class={cn(
|
|
112
|
-
"relative z-50 min-w-8rem overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-80",
|
|
109
|
+
"relative z-50 min-w-8rem overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-80 max-h-60",
|
|
113
110
|
local.class
|
|
114
111
|
)}
|
|
115
112
|
{...rest}
|
|
116
113
|
>
|
|
117
|
-
<
|
|
114
|
+
<ScrollArea class="max-h-60 w-full">
|
|
115
|
+
<SelectPrimitive.Listbox class="p-1 outline-none" />
|
|
116
|
+
</ScrollArea>
|
|
118
117
|
</SelectPrimitive.Content>
|
|
119
118
|
</SelectPrimitive.Portal>
|
|
120
119
|
);
|
|
@@ -137,16 +136,23 @@ export const SelectItem = <T extends ValidComponent = "li">(
|
|
|
137
136
|
return (
|
|
138
137
|
<SelectPrimitive.Item
|
|
139
138
|
class={cn(
|
|
140
|
-
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
|
|
139
|
+
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50 text-foreground",
|
|
141
140
|
local.class
|
|
142
141
|
)}
|
|
143
142
|
{...rest}
|
|
144
143
|
>
|
|
145
|
-
<
|
|
146
|
-
<
|
|
144
|
+
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
145
|
+
<SelectPrimitive.ItemIndicator
|
|
146
|
+
as="svg"
|
|
147
|
+
class="h-4 w-4"
|
|
148
|
+
viewBox="0 0 24 24"
|
|
149
|
+
fill="none"
|
|
150
|
+
stroke="currentColor"
|
|
151
|
+
stroke-width="2"
|
|
152
|
+
>
|
|
147
153
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
148
|
-
</
|
|
149
|
-
</
|
|
154
|
+
</SelectPrimitive.ItemIndicator>
|
|
155
|
+
</span>
|
|
150
156
|
<SelectPrimitive.ItemLabel>{local.children}</SelectPrimitive.ItemLabel>
|
|
151
157
|
</SelectPrimitive.Item>
|
|
152
158
|
);
|
|
@@ -2,6 +2,8 @@ import { splitProps, type Component, type JSX, type ValidComponent, Show } from
|
|
|
2
2
|
import * as DialogPrimitive from "@kobalte/core/dialog";
|
|
3
3
|
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
|
4
4
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { X } from "lucide-solid";
|
|
6
|
+
import { ScrollArea } from "./scroll-area";
|
|
5
7
|
import { cn } from "@/lib/cn";
|
|
6
8
|
|
|
7
9
|
// Global CSS Keyframe Animations specifically designed for Kobalte animationend DOM events
|
|
@@ -13,7 +15,7 @@ const sheetStyles = `
|
|
|
13
15
|
@keyframes sheet-slide-in-top { from { transform: translateY(-100%); } to { transform: translateY(0); } }
|
|
14
16
|
@keyframes sheet-slide-out-top { from { transform: translateY(0); } to { transform: translateY(-100%); } }
|
|
15
17
|
@keyframes sheet-slide-in-bottom { from { transform: translateY(100%); } to { transform: translateY(0); } }
|
|
16
|
-
@keyframes sheet-slide-out-bottom { from { transform: translateY(0); } to { transform: translateY(100%); } }
|
|
18
|
+
@keyframes sheet-slide-out-bottom { from { transform: translateY(0); } to { transform: translateY(-100%); } }
|
|
17
19
|
@keyframes sheet-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
18
20
|
@keyframes sheet-fade-out { from { opacity: 1; } to { opacity: 0; } }
|
|
19
21
|
`;
|
|
@@ -26,10 +28,12 @@ export const sheetVariants = cva(
|
|
|
26
28
|
{
|
|
27
29
|
variants: {
|
|
28
30
|
side: {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
top: "inset-x-0 top-0 border-b data-[expanded]:animate-[sheet-slide-in-top_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-top_300ms_ease-in-out]",
|
|
32
|
+
bottom:
|
|
33
|
+
"inset-x-0 bottom-0 border-t data-[expanded]:animate-[sheet-slide-in-bottom_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-bottom_300ms_ease-in-out]",
|
|
34
|
+
left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm data-[expanded]:animate-[sheet-slide-in-left_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-left_300ms_ease-in-out]",
|
|
35
|
+
right:
|
|
36
|
+
"inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm data-[expanded]:animate-[sheet-slide-in-right_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-right_300ms_ease-in-out]",
|
|
33
37
|
},
|
|
34
38
|
},
|
|
35
39
|
defaultVariants: {
|
|
@@ -38,23 +42,20 @@ export const sheetVariants = cva(
|
|
|
38
42
|
}
|
|
39
43
|
);
|
|
40
44
|
|
|
41
|
-
export
|
|
42
|
-
|
|
43
|
-
export const Sheet: Component<SheetRootProps> = (props) => {
|
|
44
|
-
return <DialogPrimitive.Root {...props} />;
|
|
45
|
-
};
|
|
46
|
-
|
|
45
|
+
export const Sheet = DialogPrimitive.Root;
|
|
47
46
|
export const SheetTrigger = DialogPrimitive.Trigger;
|
|
48
47
|
export const SheetClose = DialogPrimitive.CloseButton;
|
|
48
|
+
export const SheetPortal = DialogPrimitive.Portal;
|
|
49
49
|
|
|
50
|
-
export
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
export type SheetOverlayProps<T extends ValidComponent = "div"> =
|
|
51
|
+
DialogPrimitive.DialogOverlayProps<T> & {
|
|
52
|
+
/** Whether to apply backdrop blur (default: true) */
|
|
53
|
+
blur?: boolean;
|
|
54
|
+
class?: string;
|
|
55
|
+
};
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
|
-
*
|
|
58
|
+
* Fullscreen dark backdrop with fade keyframe animation.
|
|
58
59
|
*/
|
|
59
60
|
export const SheetOverlay = <T extends ValidComponent = "div">(
|
|
60
61
|
props: PolymorphicProps<T, SheetOverlayProps<T>>
|
|
@@ -64,8 +65,8 @@ export const SheetOverlay = <T extends ValidComponent = "div">(
|
|
|
64
65
|
return (
|
|
65
66
|
<DialogPrimitive.Overlay
|
|
66
67
|
class={cn(
|
|
67
|
-
"fixed inset-0 z-50 data-[expanded]:animate-[sheet-fade-in_300ms_ease-in-out] data-[closed]:animate-[sheet-fade-out_300ms_ease-in-out]",
|
|
68
|
-
local.blur !== false ? "bg-black/80 backdrop-blur-sm" : "bg-
|
|
68
|
+
"fixed inset-0 z-50 transition-all duration-200 data-[expanded]:animate-[sheet-fade-in_300ms_ease-in-out] data-[closed]:animate-[sheet-fade-out_300ms_ease-in-out]",
|
|
69
|
+
local.blur !== false ? "bg-black/80 backdrop-blur-sm" : "bg-black/80",
|
|
69
70
|
local.class
|
|
70
71
|
)}
|
|
71
72
|
{...(rest as any)}
|
|
@@ -75,32 +76,28 @@ export const SheetOverlay = <T extends ValidComponent = "div">(
|
|
|
75
76
|
|
|
76
77
|
export type SheetContentProps<T extends ValidComponent = "div"> =
|
|
77
78
|
DialogPrimitive.DialogContentProps<T> &
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
blur?: boolean;
|
|
87
|
-
class?: string;
|
|
88
|
-
children?: JSX.Element;
|
|
89
|
-
};
|
|
79
|
+
VariantProps<typeof sheetVariants> & {
|
|
80
|
+
side?: "top" | "bottom" | "left" | "right";
|
|
81
|
+
showCloseButton?: boolean;
|
|
82
|
+
closeOnOutsideClick?: boolean;
|
|
83
|
+
blur?: boolean;
|
|
84
|
+
class?: string;
|
|
85
|
+
children?: JSX.Element;
|
|
86
|
+
};
|
|
90
87
|
|
|
91
88
|
/**
|
|
92
|
-
*
|
|
89
|
+
* Slide-out panel container with ScrollArea and animation support.
|
|
93
90
|
*/
|
|
94
91
|
export const SheetContent = <T extends ValidComponent = "div">(
|
|
95
92
|
props: PolymorphicProps<T, SheetContentProps<T>>
|
|
96
93
|
) => {
|
|
97
94
|
const [local, rest] = splitProps(props as SheetContentProps, [
|
|
95
|
+
"class",
|
|
96
|
+
"children",
|
|
98
97
|
"side",
|
|
99
98
|
"showCloseButton",
|
|
100
99
|
"closeOnOutsideClick",
|
|
101
100
|
"blur",
|
|
102
|
-
"class",
|
|
103
|
-
"children",
|
|
104
101
|
"onPointerDownOutside",
|
|
105
102
|
"onInteractOutside",
|
|
106
103
|
]);
|
|
@@ -126,26 +123,28 @@ export const SheetContent = <T extends ValidComponent = "div">(
|
|
|
126
123
|
};
|
|
127
124
|
|
|
128
125
|
return (
|
|
129
|
-
<
|
|
126
|
+
<SheetPortal>
|
|
130
127
|
<style>{sheetStyles}</style>
|
|
131
128
|
<SheetOverlay blur={local.blur} />
|
|
132
129
|
<DialogPrimitive.Content
|
|
133
130
|
onPointerDownOutside={handlePointerDownOutside}
|
|
134
131
|
onInteractOutside={handleInteractOutside}
|
|
135
|
-
class={cn(sheetVariants({ side: side() }), local.class)}
|
|
132
|
+
class={cn(sheetVariants({ side: side() }), "p-0", local.class)}
|
|
136
133
|
{...(rest as any)}
|
|
137
134
|
>
|
|
138
|
-
|
|
135
|
+
<ScrollArea class="h-full w-full">
|
|
136
|
+
<div class="p-6 space-y-4">
|
|
137
|
+
{local.children}
|
|
138
|
+
</div>
|
|
139
|
+
</ScrollArea>
|
|
139
140
|
<Show when={local.showCloseButton !== false}>
|
|
140
|
-
<DialogPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none cursor-pointer">
|
|
141
|
-
<
|
|
142
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
143
|
-
</svg>
|
|
141
|
+
<DialogPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none cursor-pointer z-50">
|
|
142
|
+
<X class="h-4 w-4" />
|
|
144
143
|
<span class="sr-only">Close</span>
|
|
145
144
|
</DialogPrimitive.CloseButton>
|
|
146
145
|
</Show>
|
|
147
146
|
</DialogPrimitive.Content>
|
|
148
|
-
</
|
|
147
|
+
</SheetPortal>
|
|
149
148
|
);
|
|
150
149
|
};
|
|
151
150
|
|
|
@@ -173,44 +172,44 @@ export const SheetFooter: Component<SheetFooterProps> = (props) => {
|
|
|
173
172
|
|
|
174
173
|
return (
|
|
175
174
|
<div
|
|
176
|
-
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", local.class)}
|
|
175
|
+
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 pt-4", local.class)}
|
|
177
176
|
{...rest}
|
|
178
177
|
/>
|
|
179
178
|
);
|
|
180
179
|
};
|
|
181
180
|
|
|
182
|
-
export
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
export interface SheetTitleProps {
|
|
182
|
+
class?: string;
|
|
183
|
+
children?: JSX.Element;
|
|
184
|
+
}
|
|
186
185
|
|
|
187
|
-
export const SheetTitle
|
|
188
|
-
props
|
|
189
|
-
) => {
|
|
190
|
-
const [local, rest] = splitProps(props as SheetTitleProps, ["class"]);
|
|
186
|
+
export const SheetTitle: Component<SheetTitleProps> = (props) => {
|
|
187
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
191
188
|
|
|
192
189
|
return (
|
|
193
190
|
<DialogPrimitive.Title
|
|
194
191
|
class={cn("text-lg font-semibold text-foreground", local.class)}
|
|
195
|
-
{...
|
|
196
|
-
|
|
192
|
+
{...rest}
|
|
193
|
+
>
|
|
194
|
+
{local.children}
|
|
195
|
+
</DialogPrimitive.Title>
|
|
197
196
|
);
|
|
198
197
|
};
|
|
199
198
|
|
|
200
|
-
export
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
199
|
+
export interface SheetDescriptionProps {
|
|
200
|
+
class?: string;
|
|
201
|
+
children?: JSX.Element;
|
|
202
|
+
}
|
|
204
203
|
|
|
205
|
-
export const SheetDescription
|
|
206
|
-
props
|
|
207
|
-
) => {
|
|
208
|
-
const [local, rest] = splitProps(props as SheetDescriptionProps, ["class"]);
|
|
204
|
+
export const SheetDescription: Component<SheetDescriptionProps> = (props) => {
|
|
205
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
209
206
|
|
|
210
207
|
return (
|
|
211
208
|
<DialogPrimitive.Description
|
|
212
209
|
class={cn("text-sm text-muted-foreground", local.class)}
|
|
213
|
-
{...
|
|
214
|
-
|
|
210
|
+
{...rest}
|
|
211
|
+
>
|
|
212
|
+
{local.children}
|
|
213
|
+
</DialogPrimitive.Description>
|
|
215
214
|
);
|
|
216
215
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { splitProps, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
export const spinnerVariants = cva(
|
|
6
|
+
"inline-block animate-spin rounded-full border-2 border-current border-r-transparent text-primary",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
size: {
|
|
10
|
+
sm: "size-3",
|
|
11
|
+
default: "size-4",
|
|
12
|
+
lg: "size-6",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
size: "default",
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export interface SpinnerProps
|
|
22
|
+
extends Omit<JSX.HTMLAttributes<HTMLSpanElement>, "role">,
|
|
23
|
+
VariantProps<typeof spinnerVariants> {
|
|
24
|
+
/** Accessible text announced while the spinner is active. */
|
|
25
|
+
label?: string;
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A compact, accessible loading indicator for async UI states. */
|
|
30
|
+
export const Spinner: Component<SpinnerProps> = (props) => {
|
|
31
|
+
const [local, rest] = splitProps(props, ["size", "label", "class"]);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<span
|
|
35
|
+
role="status"
|
|
36
|
+
aria-label={local.label || "Loading"}
|
|
37
|
+
class={cn(spinnerVariants({ size: local.size }), local.class)}
|
|
38
|
+
{...rest}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Show, splitProps, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
export const statusVariants = cva(
|
|
6
|
+
"inline-flex items-center gap-2 text-sm font-medium text-foreground",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
neutral: "text-muted-foreground",
|
|
11
|
+
success: "text-emerald-700 dark:text-emerald-400",
|
|
12
|
+
warning: "text-amber-700 dark:text-amber-400",
|
|
13
|
+
error: "text-destructive",
|
|
14
|
+
info: "text-blue-700 dark:text-blue-400",
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
sm: "text-xs gap-1.5",
|
|
18
|
+
default: "text-sm gap-2",
|
|
19
|
+
},
|
|
20
|
+
bordered: {
|
|
21
|
+
true: "rounded-md border px-2 py-1",
|
|
22
|
+
false: "",
|
|
23
|
+
},
|
|
24
|
+
borderVariant: {
|
|
25
|
+
neutral: "border-muted-foreground/30",
|
|
26
|
+
success: "border-emerald-500/30",
|
|
27
|
+
warning: "border-amber-500/30",
|
|
28
|
+
error: "border-destructive/30",
|
|
29
|
+
info: "border-blue-500/30",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: "neutral",
|
|
34
|
+
size: "default",
|
|
35
|
+
bordered: false,
|
|
36
|
+
borderVariant: "neutral",
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export const statusDotVariants = cva("size-2 shrink-0 rounded-lg", {
|
|
42
|
+
variants: {
|
|
43
|
+
variant: {
|
|
44
|
+
neutral: "bg-muted-foreground",
|
|
45
|
+
success: "bg-emerald-500",
|
|
46
|
+
warning: "bg-amber-500",
|
|
47
|
+
error: "bg-destructive",
|
|
48
|
+
info: "bg-blue-500",
|
|
49
|
+
},
|
|
50
|
+
size: {
|
|
51
|
+
sm: "size-1.5",
|
|
52
|
+
default: "size-2",
|
|
53
|
+
},
|
|
54
|
+
animation: {
|
|
55
|
+
none: "",
|
|
56
|
+
pulse: "animate-pulse",
|
|
57
|
+
ping: "",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
defaultVariants: {
|
|
61
|
+
variant: "neutral",
|
|
62
|
+
size: "default",
|
|
63
|
+
animation: "none",
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export interface StatusProps
|
|
68
|
+
extends JSX.HTMLAttributes<HTMLSpanElement>,
|
|
69
|
+
VariantProps<typeof statusVariants> {
|
|
70
|
+
class?: string;
|
|
71
|
+
animation?: "none" | "pulse" | "ping";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A compact status indicator combining a semantic color dot and label. */
|
|
75
|
+
export const Status: Component<StatusProps> = (props) => {
|
|
76
|
+
const [local, rest] = splitProps(props, [
|
|
77
|
+
"variant",
|
|
78
|
+
"size",
|
|
79
|
+
"class",
|
|
80
|
+
"children",
|
|
81
|
+
"animation",
|
|
82
|
+
"bordered",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<span
|
|
87
|
+
role="status"
|
|
88
|
+
class={cn(
|
|
89
|
+
statusVariants({
|
|
90
|
+
variant: local.variant,
|
|
91
|
+
size: local.size,
|
|
92
|
+
bordered: local.bordered,
|
|
93
|
+
borderVariant: local.variant,
|
|
94
|
+
}),
|
|
95
|
+
local.class
|
|
96
|
+
)}
|
|
97
|
+
{...rest}
|
|
98
|
+
>
|
|
99
|
+
<span class="relative flex shrink-0" aria-hidden="true">
|
|
100
|
+
<Show when={local.animation === "ping"}>
|
|
101
|
+
<span
|
|
102
|
+
class={cn(
|
|
103
|
+
"absolute inline-flex size-full animate-ping rounded-lg opacity-75",
|
|
104
|
+
statusDotVariants({ variant: local.variant, size: local.size })
|
|
105
|
+
)}
|
|
106
|
+
/>
|
|
107
|
+
</Show>
|
|
108
|
+
<span
|
|
109
|
+
class={statusDotVariants({
|
|
110
|
+
variant: local.variant,
|
|
111
|
+
size: local.size,
|
|
112
|
+
animation: local.animation === "ping" ? "none" : local.animation,
|
|
113
|
+
})}
|
|
114
|
+
/>
|
|
115
|
+
</span>
|
|
116
|
+
{local.children}
|
|
117
|
+
</span>
|
|
118
|
+
);
|
|
119
|
+
};
|