@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.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/registry/aspect-ratio.json +17 -0
  3. package/registry/button.json +4 -1
  4. package/registry/collapsible.json +18 -0
  5. package/registry/combobox.json +4 -1
  6. package/registry/command.json +3 -2
  7. package/registry/context-menu.json +24 -0
  8. package/registry/create-form.json +1 -1
  9. package/registry/dialog.json +4 -1
  10. package/registry/dropdown-menu.json +4 -1
  11. package/registry/empty.json +17 -0
  12. package/registry/field.json +20 -0
  13. package/registry/form-message.json +16 -0
  14. package/registry/form.json +17 -0
  15. package/registry/icon-button.json +20 -0
  16. package/registry/index.json +191 -1
  17. package/registry/number-input.json +24 -0
  18. package/registry/resizable.json +21 -0
  19. package/registry/scroll-area.json +21 -0
  20. package/registry/select.json +4 -1
  21. package/registry/sheet.json +4 -1
  22. package/registry/spinner.json +18 -0
  23. package/registry/status.json +18 -0
  24. package/registry/theme-manager.json +1 -1
  25. package/registry/toggle.json +18 -0
  26. package/src/registry/components/ui/aspect-ratio.tsx +31 -0
  27. package/src/registry/components/ui/button.tsx +18 -3
  28. package/src/registry/components/ui/collapsible.tsx +73 -0
  29. package/src/registry/components/ui/combobox.tsx +23 -84
  30. package/src/registry/components/ui/command.tsx +154 -66
  31. package/src/registry/components/ui/context-menu.tsx +192 -0
  32. package/src/registry/components/ui/dialog.tsx +39 -44
  33. package/src/registry/components/ui/dropdown-menu.tsx +40 -45
  34. package/src/registry/components/ui/empty.tsx +83 -0
  35. package/src/registry/components/ui/field.tsx +67 -0
  36. package/src/registry/components/ui/form-message.tsx +36 -0
  37. package/src/registry/components/ui/form.tsx +21 -0
  38. package/src/registry/components/ui/icon-button.tsx +34 -0
  39. package/src/registry/components/ui/number-input.tsx +150 -0
  40. package/src/registry/components/ui/resizable.tsx +193 -0
  41. package/src/registry/components/ui/scroll-area.tsx +218 -0
  42. package/src/registry/components/ui/select.tsx +22 -16
  43. package/src/registry/components/ui/sheet.tsx +62 -63
  44. package/src/registry/components/ui/spinner.tsx +41 -0
  45. package/src/registry/components/ui/status.tsx +119 -0
  46. package/src/registry/components/ui/theme-toggle.tsx +6 -4
  47. package/src/registry/components/ui/toggle.tsx +101 -0
  48. 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 top of Kobalte headless primitives.
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: PolymorphicProps<T, SelectRootProps<Option, OptGroup, T>>
15
+ props: SelectRootProps<Option, OptGroup, T>
17
16
  ) => {
18
- const [local, rest] = splitProps(props as SelectRootProps, ["class"]);
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
- <SelectPrimitive.Listbox class="p-1 outline-none" />
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
- <SelectPrimitive.ItemIndicator class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
146
- <svg class="h-4 w-4 fill-none stroke-current stroke-2" viewBox="0 0 24 24">
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
- </svg>
149
- </SelectPrimitive.ItemIndicator>
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
- left: "fixed top-0 bottom-0 left-0 w-3/4 border-r border-border 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]",
30
- right: "fixed top-0 bottom-0 right-0 w-3/4 border-l border-border 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]",
31
- top: "fixed top-0 left-0 right-0 border-b border-border data-[expanded]:animate-[sheet-slide-in-top_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-top_300ms_ease-in-out]",
32
- bottom: "fixed bottom-0 left-0 right-0 border-t border-border data-[expanded]:animate-[sheet-slide-in-bottom_300ms_ease-in-out] data-[closed]:animate-[sheet-slide-out-bottom_300ms_ease-in-out]",
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 type SheetRootProps = DialogPrimitive.DialogRootProps;
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 interface SheetOverlayProps<T extends ValidComponent = "div"> {
51
- /** Whether to apply background blur effect or keep transparent (default: true) */
52
- blur?: boolean;
53
- class?: string;
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
- * Backdrop overlay wrapper with fade-in and fade-out animations.
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-transparent",
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
- VariantProps<typeof sheetVariants> & {
79
- /** Direction from which the sheet slides out: top, bottom, left, right */
80
- side?: "top" | "bottom" | "left" | "right";
81
- /** Whether to display the top-right close (X) button (default: true) */
82
- showCloseButton?: boolean;
83
- /** Whether clicking outside closes the sheet (default: true) */
84
- closeOnOutsideClick?: boolean;
85
- /** Whether to apply background backdrop blur (default: true) */
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
- * Main sheet container with smooth CSS keyframe slide animations.
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
- <DialogPrimitive.Portal>
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
- {local.children}
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
- <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
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
- </DialogPrimitive.Portal>
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 type SheetTitleProps<T extends ValidComponent = "h2"> =
183
- DialogPrimitive.DialogTitleProps<T> & {
184
- class?: string;
185
- };
181
+ export interface SheetTitleProps {
182
+ class?: string;
183
+ children?: JSX.Element;
184
+ }
186
185
 
187
- export const SheetTitle = <T extends ValidComponent = "h2">(
188
- props: PolymorphicProps<T, SheetTitleProps<T>>
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
- {...(rest as any)}
196
- />
192
+ {...rest}
193
+ >
194
+ {local.children}
195
+ </DialogPrimitive.Title>
197
196
  );
198
197
  };
199
198
 
200
- export type SheetDescriptionProps<T extends ValidComponent = "p"> =
201
- DialogPrimitive.DialogDescriptionProps<T> & {
202
- class?: string;
203
- };
199
+ export interface SheetDescriptionProps {
200
+ class?: string;
201
+ children?: JSX.Element;
202
+ }
204
203
 
205
- export const SheetDescription = <T extends ValidComponent = "p">(
206
- props: PolymorphicProps<T, SheetDescriptionProps<T>>
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
- {...(rest as any)}
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
+ };