@aiready/components 0.1.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/README.md +240 -0
- package/dist/charts/ForceDirectedGraph.d.ts +40 -0
- package/dist/charts/ForceDirectedGraph.js +294 -0
- package/dist/charts/ForceDirectedGraph.js.map +1 -0
- package/dist/components/badge.d.ts +13 -0
- package/dist/components/badge.js +32 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/button.d.ts +14 -0
- package/dist/components/button.js +52 -0
- package/dist/components/button.js.map +1 -0
- package/dist/components/card.d.ts +10 -0
- package/dist/components/card.js +66 -0
- package/dist/components/card.js.map +1 -0
- package/dist/components/checkbox.d.ts +8 -0
- package/dist/components/checkbox.js +42 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/container.d.ts +8 -0
- package/dist/components/container.js +36 -0
- package/dist/components/container.js.map +1 -0
- package/dist/components/grid.d.ts +9 -0
- package/dist/components/grid.js +44 -0
- package/dist/components/grid.js.map +1 -0
- package/dist/components/input.d.ts +7 -0
- package/dist/components/input.js +30 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/label.d.ts +10 -0
- package/dist/components/label.js +28 -0
- package/dist/components/label.js.map +1 -0
- package/dist/components/radio-group.d.ts +17 -0
- package/dist/components/radio-group.js +64 -0
- package/dist/components/radio-group.js.map +1 -0
- package/dist/components/select.d.ts +15 -0
- package/dist/components/select.js +45 -0
- package/dist/components/select.js.map +1 -0
- package/dist/components/separator.d.ts +9 -0
- package/dist/components/separator.js +30 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/stack.d.ts +11 -0
- package/dist/components/stack.js +60 -0
- package/dist/components/stack.js.map +1 -0
- package/dist/components/switch.d.ts +9 -0
- package/dist/components/switch.js +49 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/textarea.d.ts +7 -0
- package/dist/components/textarea.js +29 -0
- package/dist/components/textarea.js.map +1 -0
- package/dist/hooks/useD3.d.ts +6 -0
- package/dist/hooks/useD3.js +35 -0
- package/dist/hooks/useD3.js.map +1 -0
- package/dist/hooks/useDebounce.d.ts +3 -0
- package/dist/hooks/useDebounce.js +19 -0
- package/dist/hooks/useDebounce.js.map +1 -0
- package/dist/hooks/useForceSimulation.d.ts +39 -0
- package/dist/hooks/useForceSimulation.js +107 -0
- package/dist/hooks/useForceSimulation.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +927 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/cn.d.ts +5 -0
- package/dist/utils/cn.js +11 -0
- package/dist/utils/cn.js.map +1 -0
- package/dist/utils/colors.d.ts +19 -0
- package/dist/utils/colors.js +52 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/utils/formatters.d.ts +13 -0
- package/dist/utils/formatters.js +100 -0
- package/dist/utils/formatters.js.map +1 -0
- package/package.json +83 -0
- package/src/charts/ForceDirectedGraph.tsx +356 -0
- package/src/components/badge.tsx +35 -0
- package/src/components/button.tsx +53 -0
- package/src/components/card.tsx +78 -0
- package/src/components/checkbox.tsx +39 -0
- package/src/components/container.tsx +31 -0
- package/src/components/grid.tsx +40 -0
- package/src/components/input.tsx +24 -0
- package/src/components/label.tsx +24 -0
- package/src/components/radio-group.tsx +71 -0
- package/src/components/select.tsx +53 -0
- package/src/components/separator.tsx +29 -0
- package/src/components/stack.tsx +61 -0
- package/src/components/switch.tsx +49 -0
- package/src/components/textarea.tsx +23 -0
- package/src/hooks/useD3.ts +125 -0
- package/src/hooks/useDebounce.ts +44 -0
- package/src/hooks/useForceSimulation.ts +328 -0
- package/src/index.ts +51 -0
- package/src/utils/cn.ts +11 -0
- package/src/utils/colors.ts +58 -0
- package/src/utils/formatters.ts +161 -0
- package/tailwind.config.js +46 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export interface RadioOption {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RadioGroupProps
|
|
11
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
12
|
+
options: RadioOption[];
|
|
13
|
+
value?: string;
|
|
14
|
+
onChange?: (value: string) => void;
|
|
15
|
+
name: string;
|
|
16
|
+
orientation?: 'horizontal' | 'vertical';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
|
|
20
|
+
(
|
|
21
|
+
{
|
|
22
|
+
className,
|
|
23
|
+
options,
|
|
24
|
+
value,
|
|
25
|
+
onChange,
|
|
26
|
+
name,
|
|
27
|
+
orientation = 'vertical',
|
|
28
|
+
...props
|
|
29
|
+
},
|
|
30
|
+
ref
|
|
31
|
+
) => {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
ref={ref}
|
|
35
|
+
className={cn(
|
|
36
|
+
'flex',
|
|
37
|
+
orientation === 'vertical' ? 'flex-col gap-2' : 'flex-row gap-4',
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
{options.map((option) => {
|
|
43
|
+
const id = `${name}-${option.value}`;
|
|
44
|
+
return (
|
|
45
|
+
<div key={option.value} className="flex items-center">
|
|
46
|
+
<input
|
|
47
|
+
type="radio"
|
|
48
|
+
id={id}
|
|
49
|
+
name={name}
|
|
50
|
+
value={option.value}
|
|
51
|
+
checked={value === option.value}
|
|
52
|
+
disabled={option.disabled}
|
|
53
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
54
|
+
className="h-4 w-4 border-gray-300 text-primary focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
55
|
+
/>
|
|
56
|
+
<label
|
|
57
|
+
htmlFor={id}
|
|
58
|
+
className="ml-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
59
|
+
>
|
|
60
|
+
{option.label}
|
|
61
|
+
</label>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
RadioGroup.displayName = 'RadioGroup';
|
|
70
|
+
|
|
71
|
+
export { RadioGroup };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export interface SelectOption {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SelectProps
|
|
11
|
+
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'onChange'> {
|
|
12
|
+
options: SelectOption[];
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
onChange?: (value: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|
18
|
+
({ className, options, placeholder, onChange, ...props }, ref) => {
|
|
19
|
+
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
20
|
+
onChange?.(e.target.value);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<select
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn(
|
|
27
|
+
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
onChange={handleChange}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
{placeholder && (
|
|
34
|
+
<option value="" disabled>
|
|
35
|
+
{placeholder}
|
|
36
|
+
</option>
|
|
37
|
+
)}
|
|
38
|
+
{options.map((option) => (
|
|
39
|
+
<option
|
|
40
|
+
key={option.value}
|
|
41
|
+
value={option.value}
|
|
42
|
+
disabled={option.disabled}
|
|
43
|
+
>
|
|
44
|
+
{option.label}
|
|
45
|
+
</option>
|
|
46
|
+
))}
|
|
47
|
+
</select>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
Select.displayName = 'Select';
|
|
52
|
+
|
|
53
|
+
export { Select };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
orientation?: 'horizontal' | 'vertical';
|
|
6
|
+
decorative?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
|
|
10
|
+
(
|
|
11
|
+
{ className, orientation = 'horizontal', decorative = true, ...props },
|
|
12
|
+
ref
|
|
13
|
+
) => (
|
|
14
|
+
<div
|
|
15
|
+
ref={ref}
|
|
16
|
+
role={decorative ? 'none' : 'separator'}
|
|
17
|
+
aria-orientation={orientation}
|
|
18
|
+
className={cn(
|
|
19
|
+
'shrink-0 bg-border',
|
|
20
|
+
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
Separator.displayName = 'Separator';
|
|
28
|
+
|
|
29
|
+
export { Separator };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export interface StackProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
direction?: 'horizontal' | 'vertical';
|
|
6
|
+
spacing?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
7
|
+
align?: 'start' | 'center' | 'end' | 'stretch';
|
|
8
|
+
justify?: 'start' | 'center' | 'end' | 'between' | 'around';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Stack = React.forwardRef<HTMLDivElement, StackProps>(
|
|
12
|
+
(
|
|
13
|
+
{
|
|
14
|
+
className,
|
|
15
|
+
direction = 'vertical',
|
|
16
|
+
spacing = 'md',
|
|
17
|
+
align = 'stretch',
|
|
18
|
+
justify = 'start',
|
|
19
|
+
...props
|
|
20
|
+
},
|
|
21
|
+
ref
|
|
22
|
+
) => {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn(
|
|
27
|
+
'flex',
|
|
28
|
+
{
|
|
29
|
+
'flex-col': direction === 'vertical',
|
|
30
|
+
'flex-row': direction === 'horizontal',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
'gap-1': spacing === 'xs',
|
|
34
|
+
'gap-2': spacing === 'sm',
|
|
35
|
+
'gap-4': spacing === 'md',
|
|
36
|
+
'gap-6': spacing === 'lg',
|
|
37
|
+
'gap-8': spacing === 'xl',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
'items-start': align === 'start',
|
|
41
|
+
'items-center': align === 'center',
|
|
42
|
+
'items-end': align === 'end',
|
|
43
|
+
'items-stretch': align === 'stretch',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
'justify-start': justify === 'start',
|
|
47
|
+
'justify-center': justify === 'center',
|
|
48
|
+
'justify-end': justify === 'end',
|
|
49
|
+
'justify-between': justify === 'between',
|
|
50
|
+
'justify-around': justify === 'around',
|
|
51
|
+
},
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
Stack.displayName = 'Stack';
|
|
60
|
+
|
|
61
|
+
export { Stack };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export interface SwitchProps
|
|
5
|
+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
6
|
+
label?: string;
|
|
7
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
|
|
11
|
+
({ className, label, id, checked, onCheckedChange, onChange, ...props }, ref) => {
|
|
12
|
+
const switchId = id || React.useId();
|
|
13
|
+
|
|
14
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
15
|
+
onChange?.(e);
|
|
16
|
+
onCheckedChange?.(e.target.checked);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex items-center">
|
|
21
|
+
<label htmlFor={switchId} className="relative inline-flex cursor-pointer items-center">
|
|
22
|
+
<input
|
|
23
|
+
type="checkbox"
|
|
24
|
+
id={switchId}
|
|
25
|
+
ref={ref}
|
|
26
|
+
checked={checked}
|
|
27
|
+
onChange={handleChange}
|
|
28
|
+
className="peer sr-only"
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
<div
|
|
32
|
+
className={cn(
|
|
33
|
+
'peer h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[""] peer-checked:bg-primary peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-ring peer-focus:ring-offset-2 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
/>
|
|
37
|
+
</label>
|
|
38
|
+
{label && (
|
|
39
|
+
<span className="ml-3 text-sm font-medium text-foreground">
|
|
40
|
+
{label}
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
Switch.displayName = 'Switch';
|
|
48
|
+
|
|
49
|
+
export { Switch };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export interface TextareaProps
|
|
5
|
+
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
6
|
+
|
|
7
|
+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
8
|
+
({ className, ...props }, ref) => {
|
|
9
|
+
return (
|
|
10
|
+
<textarea
|
|
11
|
+
className={cn(
|
|
12
|
+
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
ref={ref}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
Textarea.displayName = 'Textarea';
|
|
22
|
+
|
|
23
|
+
export { Textarea };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import * as d3 from 'd3';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook for managing D3 selections with React lifecycle
|
|
6
|
+
* Provides a ref to the SVG/container element and runs a render function when dependencies change
|
|
7
|
+
*
|
|
8
|
+
* @param renderFn - Function that receives the D3 selection and performs rendering
|
|
9
|
+
* @param dependencies - Array of dependencies that trigger re-render
|
|
10
|
+
* @returns Ref to attach to the SVG/container element
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* function BarChart({ data }: { data: number[] }) {
|
|
15
|
+
* const ref = useD3(
|
|
16
|
+
* (svg) => {
|
|
17
|
+
* const width = 600;
|
|
18
|
+
* const height = 400;
|
|
19
|
+
* const margin = { top: 20, right: 20, bottom: 30, left: 40 };
|
|
20
|
+
*
|
|
21
|
+
* // Clear previous content
|
|
22
|
+
* svg.selectAll('*').remove();
|
|
23
|
+
*
|
|
24
|
+
* // Set up scales
|
|
25
|
+
* const x = d3.scaleBand()
|
|
26
|
+
* .domain(data.map((_, i) => i.toString()))
|
|
27
|
+
* .range([margin.left, width - margin.right])
|
|
28
|
+
* .padding(0.1);
|
|
29
|
+
*
|
|
30
|
+
* const y = d3.scaleLinear()
|
|
31
|
+
* .domain([0, d3.max(data) || 0])
|
|
32
|
+
* .range([height - margin.bottom, margin.top]);
|
|
33
|
+
*
|
|
34
|
+
* // Draw bars
|
|
35
|
+
* svg.selectAll('rect')
|
|
36
|
+
* .data(data)
|
|
37
|
+
* .join('rect')
|
|
38
|
+
* .attr('x', (_, i) => x(i.toString())!)
|
|
39
|
+
* .attr('y', d => y(d))
|
|
40
|
+
* .attr('width', x.bandwidth())
|
|
41
|
+
* .attr('height', d => y(0) - y(d))
|
|
42
|
+
* .attr('fill', 'steelblue');
|
|
43
|
+
* },
|
|
44
|
+
* [data]
|
|
45
|
+
* );
|
|
46
|
+
*
|
|
47
|
+
* return <svg ref={ref} width={600} height={400} />;
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function useD3<T extends SVGSVGElement | HTMLDivElement>(
|
|
52
|
+
renderFn: (selection: d3.Selection<T, unknown, null, undefined>) => void,
|
|
53
|
+
dependencies: React.DependencyList = []
|
|
54
|
+
): React.RefObject<T | null> {
|
|
55
|
+
const ref = useRef<T | null>(null);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (ref.current) {
|
|
59
|
+
const selection = d3.select(ref.current);
|
|
60
|
+
renderFn(selection);
|
|
61
|
+
}
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
+
}, dependencies);
|
|
64
|
+
|
|
65
|
+
return ref;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hook for managing D3 selections with automatic resize handling
|
|
70
|
+
* Similar to useD3 but also triggers re-render on window resize
|
|
71
|
+
*
|
|
72
|
+
* @param renderFn - Function that receives the D3 selection and performs rendering
|
|
73
|
+
* @param dependencies - Array of dependencies that trigger re-render
|
|
74
|
+
* @returns Ref to attach to the SVG/container element
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```tsx
|
|
78
|
+
* function ResponsiveChart({ data }: { data: number[] }) {
|
|
79
|
+
* const ref = useD3WithResize(
|
|
80
|
+
* (svg) => {
|
|
81
|
+
* const container = svg.node();
|
|
82
|
+
* const width = container?.clientWidth || 600;
|
|
83
|
+
* const height = container?.clientHeight || 400;
|
|
84
|
+
*
|
|
85
|
+
* // Render with responsive dimensions
|
|
86
|
+
* // ...
|
|
87
|
+
* },
|
|
88
|
+
* [data]
|
|
89
|
+
* );
|
|
90
|
+
*
|
|
91
|
+
* return <svg ref={ref} style={{ width: '100%', height: '400px' }} />;
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function useD3WithResize<T extends SVGSVGElement | HTMLDivElement>(
|
|
96
|
+
renderFn: (selection: d3.Selection<T, unknown, null, undefined>) => void,
|
|
97
|
+
dependencies: React.DependencyList = []
|
|
98
|
+
): React.RefObject<T | null> {
|
|
99
|
+
const ref = useRef<T | null>(null);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!ref.current) return;
|
|
103
|
+
|
|
104
|
+
const selection = d3.select(ref.current);
|
|
105
|
+
const render = () => renderFn(selection);
|
|
106
|
+
|
|
107
|
+
// Initial render
|
|
108
|
+
render();
|
|
109
|
+
|
|
110
|
+
// Set up resize observer
|
|
111
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
112
|
+
render();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
resizeObserver.observe(ref.current);
|
|
116
|
+
|
|
117
|
+
// Cleanup
|
|
118
|
+
return () => {
|
|
119
|
+
resizeObserver.disconnect();
|
|
120
|
+
};
|
|
121
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
122
|
+
}, dependencies);
|
|
123
|
+
|
|
124
|
+
return ref;
|
|
125
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Debounce a value with a specified delay
|
|
5
|
+
* Useful for search inputs, filters, and other frequently changing values
|
|
6
|
+
*
|
|
7
|
+
* @param value - The value to debounce
|
|
8
|
+
* @param delay - Delay in milliseconds (default: 300ms)
|
|
9
|
+
* @returns The debounced value
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* function SearchInput() {
|
|
14
|
+
* const [searchTerm, setSearchTerm] = useState('');
|
|
15
|
+
* const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
|
16
|
+
*
|
|
17
|
+
* useEffect(() => {
|
|
18
|
+
* // This will only run when user stops typing for 500ms
|
|
19
|
+
* if (debouncedSearchTerm) {
|
|
20
|
+
* performSearch(debouncedSearchTerm);
|
|
21
|
+
* }
|
|
22
|
+
* }, [debouncedSearchTerm]);
|
|
23
|
+
*
|
|
24
|
+
* return <input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />;
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function useDebounce<T>(value: T, delay: number = 300): T {
|
|
29
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
// Set up the timeout to update debounced value after delay
|
|
33
|
+
const timer = setTimeout(() => {
|
|
34
|
+
setDebouncedValue(value);
|
|
35
|
+
}, delay);
|
|
36
|
+
|
|
37
|
+
// Clean up the timeout if value changes or component unmounts
|
|
38
|
+
return () => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
};
|
|
41
|
+
}, [value, delay]);
|
|
42
|
+
|
|
43
|
+
return debouncedValue;
|
|
44
|
+
}
|