@nikala-ui/core 0.9.11 → 0.9.12

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.
@@ -0,0 +1,193 @@
1
+ import {
2
+ createSignal,
3
+ createContext,
4
+ useContext,
5
+ splitProps,
6
+ type Component,
7
+ type JSX,
8
+ type Accessor,
9
+ } from "solid-js";
10
+ import { createElementSize } from "@nikala-ui/hooks";
11
+ import { GripVertical, GripHorizontal } from "lucide-solid";
12
+ import { cn } from "@/lib/cn";
13
+
14
+ export interface ResizableContextValue {
15
+ orientation: Accessor<"horizontal" | "vertical">;
16
+ registerPanel: (id: string, initialSizes: number) => void;
17
+ sizes: Accessor<Record<string, number>>;
18
+ startDragging: (handleIndex: number, event: PointerEvent) => void;
19
+ containerRef: () => HTMLDivElement | undefined;
20
+ }
21
+
22
+ const ResizableContext = createContext<ResizableContextValue>();
23
+
24
+ export interface ResizableGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {
25
+ orientation?: "horizontal" | "vertical";
26
+ class?: string;
27
+ children?: JSX.Element;
28
+ }
29
+
30
+ export const ResizableGroup: Component<ResizableGroupProps> = (props) => {
31
+ const [local, rest] = splitProps(props, ["orientation", "class", "children"]);
32
+ const orientation = () => local.orientation || "horizontal";
33
+ let containerEl: HTMLDivElement | undefined;
34
+
35
+ const [panelOrder, setPanelOrder] = createSignal<string[]>([]);
36
+ const [sizes, setSizes] = createSignal<Record<string, number>>({});
37
+
38
+ const registerPanel = (id: string, initialSize: number) => {
39
+ setPanelOrder((prev) => (prev.includes(id) ? prev : [...prev, id]));
40
+ setSizes((prev) => (prev[id] !== undefined ? prev : { ...prev, [id]: initialSize }));
41
+ };
42
+
43
+ const startDragging = (handleIndex: number, event: PointerEvent) => {
44
+ if (!containerEl) return;
45
+ event.preventDefault();
46
+
47
+ const order = panelOrder();
48
+ if (handleIndex < 0 || handleIndex >= order.length - 1) return;
49
+
50
+ const leftId = order[handleIndex];
51
+ const rightId = order[handleIndex + 1];
52
+
53
+ const isHoriz = orientation() === "horizontal";
54
+ const startPos = isHoriz ? event.clientX : event.clientY;
55
+ const rect = containerEl.getBoundingClientRect();
56
+ const totalPx = isHoriz ? rect.width : rect.height;
57
+
58
+ const startLeftPct = sizes()[leftId] ?? 50;
59
+ const startRightPct = sizes()[rightId] ?? 50;
60
+
61
+ const onPointerMove = (e: PointerEvent) => {
62
+ const currentPos = isHoriz ? e.clientX : e.clientY;
63
+ const deltaPx = currentPos - startPos;
64
+ const deltaPct = (deltaPx / totalPx) * 100;
65
+
66
+ let newLeft = startLeftPct + deltaPct;
67
+ let newRight = startRightPct - deltaPct;
68
+
69
+ if (newLeft < 10) {
70
+ newLeft = 10;
71
+ newRight = startLeftPct + startRightPct - 10;
72
+ } else if (newRight < 10) {
73
+ newRight = 10;
74
+ newLeft = startLeftPct + startRightPct - 10;
75
+ }
76
+
77
+ setSizes((prev) => ({
78
+ ...prev,
79
+ [leftId]: newLeft,
80
+ [rightId]: newRight,
81
+ }));
82
+ };
83
+
84
+ const onPointerUp = () => {
85
+ window.removeEventListener("pointermove", onPointerMove);
86
+ window.removeEventListener("pointerup", onPointerUp);
87
+ };
88
+
89
+ window.addEventListener("pointermove", onPointerMove);
90
+ window.addEventListener("pointerup", onPointerUp);
91
+ };
92
+
93
+ return (
94
+ <ResizableContext.Provider
95
+ value={{
96
+ orientation,
97
+ registerPanel,
98
+ sizes,
99
+ startDragging,
100
+ containerRef: () => containerEl,
101
+ }}
102
+ >
103
+ <div
104
+ ref={containerEl}
105
+ class={cn(
106
+ "flex h-full w-full overflow-hidden rounded-lg border border-border bg-background",
107
+ orientation() === "vertical" ? "flex-col" : "flex-row",
108
+ local.class
109
+ )}
110
+ {...rest}
111
+ >
112
+ {local.children}
113
+ </div>
114
+ </ResizableContext.Provider>
115
+ );
116
+ };
117
+
118
+ export interface ResizablePanelProps extends JSX.HTMLAttributes<HTMLDivElement> {
119
+ id: string;
120
+ initialSize?: number;
121
+ class?: string;
122
+ children?: JSX.Element;
123
+ }
124
+
125
+ export const ResizablePanel: Component<ResizablePanelProps> = (props) => {
126
+ const [local, rest] = splitProps(props, ["id", "initialSize", "class", "children"]);
127
+ const ctx = useContext(ResizableContext);
128
+
129
+ if (!ctx) {
130
+ throw new Error("ResizablePanel must be used within a ResizableGroup");
131
+ }
132
+
133
+ ctx.registerPanel(local.id, local.initialSize ?? 50);
134
+
135
+ const currentPct = () => ctx.sizes()[local.id] ?? local.initialSize ?? 50;
136
+
137
+ const containerSize = createElementSize(() => ctx.containerRef());
138
+
139
+ return (
140
+ <div
141
+ class={cn("overflow-auto transition-[flex-basis] duration-75", local.class)}
142
+ style={{
143
+ "flex-basis": `${currentPct()}%`,
144
+ "flex-grow": 0,
145
+ "flex-shrink": 0,
146
+ }}
147
+ {...rest}
148
+ >
149
+ {local.children}
150
+ </div>
151
+ );
152
+ };
153
+
154
+ export interface ResizableHandleProps extends JSX.HTMLAttributes<HTMLDivElement> {
155
+ handleIndex: number;
156
+ withHandle?: boolean;
157
+ class?: string;
158
+ }
159
+
160
+ export const ResizableHandle: Component<ResizableHandleProps> = (props) => {
161
+ const [local, rest] = splitProps(props, ["handleIndex", "withHandle", "class"]);
162
+ const ctx = useContext(ResizableContext);
163
+
164
+ if (!ctx) {
165
+ throw new Error("ResizableHandle must be used within a ResizableGroup");
166
+ }
167
+
168
+ const isHoriz = () => ctx.orientation() === "horizontal";
169
+
170
+ return (
171
+ <div
172
+ role="separator"
173
+ tabIndex={0}
174
+ class={cn(
175
+ "relative flex select-none items-center justify-center bg-border transition-colors hover:bg-primary/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-col-resize",
176
+ isHoriz() ? "h-full w-1.5 cursor-col-resize" : "h-1.5 w-full cursor-row-resize",
177
+ local.class
178
+ )}
179
+ onPointerDown={(e) => ctx.startDragging(local.handleIndex, e)}
180
+ {...rest}
181
+ >
182
+ {local.withHandle && (
183
+ <div class="z-10 flex h-4 w-3 items-center justify-center rounded-xs border border-border bg-muted shadow-2xs">
184
+ {isHoriz() ? (
185
+ <GripVertical class="h-2.5 w-2.5 text-muted-foreground" />
186
+ ) : (
187
+ <GripHorizontal class="h-2.5 w-2.5 text-muted-foreground" />
188
+ )}
189
+ </div>
190
+ )}
191
+ </div>
192
+ );
193
+ };
@@ -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
  );