@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,67 @@
|
|
|
1
|
+
import { splitProps, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
import { Label, type LabelProps } from "./label";
|
|
4
|
+
|
|
5
|
+
export interface FieldProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A consistent layout wrapper for labels, controls, descriptions, and errors. */
|
|
10
|
+
export const Field: Component<FieldProps> = (props) => {
|
|
11
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div class={cn("grid w-full gap-1.5", local.class)} {...rest}>
|
|
15
|
+
{local.children}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface FieldLabelProps extends LabelProps {
|
|
21
|
+
class?: string;
|
|
22
|
+
children?: JSX.Element;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const FieldLabel: Component<FieldLabelProps> = (props) => {
|
|
26
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Label class={cn("text-sm font-medium", local.class)} {...rest}>
|
|
30
|
+
{local.children}
|
|
31
|
+
</Label>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export interface FieldDescriptionProps
|
|
36
|
+
extends JSX.HTMLAttributes<HTMLParagraphElement> {
|
|
37
|
+
class?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const FieldDescription: Component<FieldDescriptionProps> = (props) => {
|
|
41
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<p class={cn("text-sm text-muted-foreground", local.class)} {...rest}>
|
|
45
|
+
{local.children}
|
|
46
|
+
</p>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export interface FieldErrorProps
|
|
51
|
+
extends JSX.HTMLAttributes<HTMLParagraphElement> {
|
|
52
|
+
class?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const FieldError: Component<FieldErrorProps> = (props) => {
|
|
56
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<p
|
|
60
|
+
role="alert"
|
|
61
|
+
class={cn("text-sm font-medium text-destructive", local.class)}
|
|
62
|
+
{...rest}
|
|
63
|
+
>
|
|
64
|
+
{local.children}
|
|
65
|
+
</p>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Show, splitProps, type JSX } from "solid-js";
|
|
2
|
+
import type { CreateFormReturn } from "@nikala-ui/hooks";
|
|
3
|
+
import { FieldError, type FieldErrorProps } from "./field";
|
|
4
|
+
|
|
5
|
+
type FormState<T extends Record<string, any>> = Pick<
|
|
6
|
+
CreateFormReturn<T>,
|
|
7
|
+
"errors" | "touched"
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
export interface FormMessageProps<
|
|
11
|
+
T extends Record<string, any>,
|
|
12
|
+
K extends keyof T,
|
|
13
|
+
> extends Omit<FieldErrorProps, "children"> {
|
|
14
|
+
form: FormState<T>;
|
|
15
|
+
name: K;
|
|
16
|
+
/** Show the message even before the field has been touched. */
|
|
17
|
+
showUntouched?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Displays a field's validation error from createForm when it should be visible. */
|
|
21
|
+
export function FormMessage<
|
|
22
|
+
T extends Record<string, any>,
|
|
23
|
+
K extends keyof T,
|
|
24
|
+
>(props: FormMessageProps<T, K>): JSX.Element {
|
|
25
|
+
const [local, rest] = splitProps(props, ["form", "name", "showUntouched", "class"]);
|
|
26
|
+
const error = () => local.form.errors()[local.name] as string | undefined;
|
|
27
|
+
const touched = () => local.form.touched()[local.name] === true;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Show when={error() && (local.showUntouched || touched())}>
|
|
31
|
+
<FieldError class={local.class} {...rest}>
|
|
32
|
+
{error()}
|
|
33
|
+
</FieldError>
|
|
34
|
+
</Show>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { splitProps, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface FormProps extends JSX.FormHTMLAttributes<HTMLFormElement> {
|
|
5
|
+
/** Indicates that the form is currently processing a submission. */
|
|
6
|
+
loading?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A semantic form layout wrapper designed to work with the createForm hook. */
|
|
11
|
+
export const Form: Component<FormProps> = (props) => {
|
|
12
|
+
const [local, rest] = splitProps(props, ["loading", "class"]);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<form
|
|
16
|
+
aria-busy={local.loading ? "true" : undefined}
|
|
17
|
+
class={cn("w-full space-y-5", local.class)}
|
|
18
|
+
{...rest}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { splitProps, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { Button, type ButtonProps } from "./button";
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
export interface IconButtonProps
|
|
6
|
+
extends Omit<ButtonProps, "size" | "children"> {
|
|
7
|
+
/** Accessible label for the icon-only button. */
|
|
8
|
+
label: string;
|
|
9
|
+
size?: "sm" | "default" | "lg";
|
|
10
|
+
children?: JSX.Element;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** An accessible square button intended for a single icon action. */
|
|
14
|
+
export const IconButton: Component<IconButtonProps> = (props) => {
|
|
15
|
+
const [local, rest] = splitProps(props, ["label", "size", "class", "children"]);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Button
|
|
19
|
+
size="icon"
|
|
20
|
+
aria-label={local.label}
|
|
21
|
+
class={cn(
|
|
22
|
+
{
|
|
23
|
+
"size-8": local.size === "sm",
|
|
24
|
+
"size-9": !local.size || local.size === "default",
|
|
25
|
+
"size-10": local.size === "lg",
|
|
26
|
+
},
|
|
27
|
+
local.class
|
|
28
|
+
)}
|
|
29
|
+
{...rest}
|
|
30
|
+
>
|
|
31
|
+
{local.children}
|
|
32
|
+
</Button>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createSignal, onCleanup, splitProps, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { NumberField as KobalteNumberField } from "@kobalte/core/number-field";
|
|
3
|
+
import { Plus, Minus } from "lucide-solid";
|
|
4
|
+
import { Input } from "./input";
|
|
5
|
+
import { Button } from "./button";
|
|
6
|
+
import { createLongPress } from "@nikala-ui/hooks";
|
|
7
|
+
import { cn } from "@/lib/cn";
|
|
8
|
+
|
|
9
|
+
export interface NumberInputProps {
|
|
10
|
+
value?: number;
|
|
11
|
+
defaultValue?: number;
|
|
12
|
+
onValueChange?: (value: number) => void;
|
|
13
|
+
minValue?: number;
|
|
14
|
+
maxValue?: number;
|
|
15
|
+
step?: number;
|
|
16
|
+
allowNegative?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
readOnly?: boolean;
|
|
19
|
+
class?: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const NumberInput: Component<NumberInputProps> = (props) => {
|
|
24
|
+
const [local, rest] = splitProps(props, [
|
|
25
|
+
"value",
|
|
26
|
+
"defaultValue",
|
|
27
|
+
"onValueChange",
|
|
28
|
+
"minValue",
|
|
29
|
+
"maxValue",
|
|
30
|
+
"step",
|
|
31
|
+
"allowNegative",
|
|
32
|
+
"disabled",
|
|
33
|
+
"readOnly",
|
|
34
|
+
"class",
|
|
35
|
+
"id",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const [internalVal, setInternalVal] = createSignal<number>(
|
|
39
|
+
local.value !== undefined
|
|
40
|
+
? local.value
|
|
41
|
+
: local.defaultValue !== undefined
|
|
42
|
+
? local.defaultValue
|
|
43
|
+
: 0
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
let autoRepeatInterval: ReturnType<typeof setInterval> | undefined;
|
|
47
|
+
|
|
48
|
+
const currentVal = () => (local.value !== undefined ? local.value : internalVal());
|
|
49
|
+
|
|
50
|
+
const effectiveMin = () => {
|
|
51
|
+
if (local.minValue !== undefined) return local.minValue;
|
|
52
|
+
return local.allowNegative ? -Infinity : 0;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const effectiveMax = () => {
|
|
56
|
+
if (local.maxValue !== undefined) return local.maxValue;
|
|
57
|
+
return Infinity;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleValueChange = (val: number) => {
|
|
61
|
+
if (isNaN(val)) return;
|
|
62
|
+
const clamped = Math.max(effectiveMin(), Math.min(effectiveMax(), val));
|
|
63
|
+
setInternalVal(clamped);
|
|
64
|
+
local.onValueChange?.(clamped);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const stopAutoRepeat = () => {
|
|
68
|
+
if (autoRepeatInterval) {
|
|
69
|
+
clearInterval(autoRepeatInterval);
|
|
70
|
+
autoRepeatInterval = undefined;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const startAutoRepeat = (direction: 1 | -1) => {
|
|
75
|
+
if (local.disabled || local.readOnly) return;
|
|
76
|
+
stopAutoRepeat();
|
|
77
|
+
const stepAmount = local.step || 1;
|
|
78
|
+
autoRepeatInterval = setInterval(() => {
|
|
79
|
+
handleValueChange(currentVal() + direction * stepAmount);
|
|
80
|
+
}, 75);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/* Nikala UI createLongPress hook for Increment Trigger */
|
|
84
|
+
const incrementLongPress = createLongPress(
|
|
85
|
+
() => {
|
|
86
|
+
startAutoRepeat(1);
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
threshold: 300,
|
|
90
|
+
onCancel: stopAutoRepeat,
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
/* Nikala UI createLongPress hook for Decrement Trigger */
|
|
95
|
+
const decrementLongPress = createLongPress(
|
|
96
|
+
() => {
|
|
97
|
+
startAutoRepeat(-1);
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
threshold: 300,
|
|
101
|
+
onCancel: stopAutoRepeat,
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
onCleanup(() => stopAutoRepeat());
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<KobalteNumberField
|
|
109
|
+
value={currentVal()}
|
|
110
|
+
onRawValueChange={handleValueChange}
|
|
111
|
+
minValue={effectiveMin()}
|
|
112
|
+
maxValue={effectiveMax()}
|
|
113
|
+
step={local.step || 1}
|
|
114
|
+
disabled={local.disabled}
|
|
115
|
+
readOnly={local.readOnly}
|
|
116
|
+
id={local.id}
|
|
117
|
+
class={cn("relative flex items-center max-w-[160px]", local.class)}
|
|
118
|
+
{...rest}
|
|
119
|
+
>
|
|
120
|
+
<KobalteNumberField.Input
|
|
121
|
+
as={Input}
|
|
122
|
+
class="pr-16 text-center font-mono focus-visible:ring-1"
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
<div class="absolute right-1 flex items-center gap-0.5">
|
|
126
|
+
<KobalteNumberField.DecrementTrigger
|
|
127
|
+
as={Button}
|
|
128
|
+
variant="ghost"
|
|
129
|
+
size="icon"
|
|
130
|
+
class="h-7 w-7 rounded-sm p-0 text-muted-foreground hover:text-foreground select-none"
|
|
131
|
+
aria-label="Decrement value"
|
|
132
|
+
{...decrementLongPress.props}
|
|
133
|
+
>
|
|
134
|
+
<Minus class="h-3.5 w-3.5" />
|
|
135
|
+
</KobalteNumberField.DecrementTrigger>
|
|
136
|
+
|
|
137
|
+
<KobalteNumberField.IncrementTrigger
|
|
138
|
+
as={Button}
|
|
139
|
+
variant="ghost"
|
|
140
|
+
size="icon"
|
|
141
|
+
class="h-7 w-7 rounded-sm p-0 text-muted-foreground hover:text-foreground select-none"
|
|
142
|
+
aria-label="Increment value"
|
|
143
|
+
{...incrementLongPress.props}
|
|
144
|
+
>
|
|
145
|
+
<Plus class="h-3.5 w-3.5" />
|
|
146
|
+
</KobalteNumberField.IncrementTrigger>
|
|
147
|
+
</div>
|
|
148
|
+
</KobalteNumberField>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
@@ -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
|
+
};
|