xertica-ui 1.0.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/App.tsx +182 -0
- package/README.md +330 -0
- package/assets/xertica-logo.svg +38 -0
- package/assets/xertica-x-logo.svg +21 -0
- package/bin/cli.ts +193 -0
- package/components/AssistenteXertica.tsx +2003 -0
- package/components/AudioPlayer.tsx +203 -0
- package/components/CodeBlock.tsx +242 -0
- package/components/DocumentEditor.tsx +504 -0
- package/components/ForgotPasswordPage.tsx +170 -0
- package/components/FormattedDocument.tsx +87 -0
- package/components/HomeContent.tsx +123 -0
- package/components/HomePage.tsx +70 -0
- package/components/LanguageSelector.tsx +54 -0
- package/components/LoginPage.tsx +199 -0
- package/components/MarkdownMessage.tsx +62 -0
- package/components/ModernChatInput.tsx +502 -0
- package/components/PodcastPlayer.tsx +409 -0
- package/components/ResetPasswordPage.tsx +234 -0
- package/components/Sidebar.tsx +489 -0
- package/components/TemplateContent.tsx +629 -0
- package/components/TemplatePage.tsx +70 -0
- package/components/ThemeToggle.tsx +65 -0
- package/components/VerifyEmailPage.tsx +187 -0
- package/components/XerticaLogo.tsx +69 -0
- package/components/XerticaOrbe.tsx +1339 -0
- package/components/XerticaXLogo.tsx +53 -0
- package/components/examples/DrawingMapExample.tsx +530 -0
- package/components/examples/FilterableMapExample.tsx +380 -0
- package/components/examples/LocationPickerExample.tsx +330 -0
- package/components/examples/MapExamples.tsx +280 -0
- package/components/examples/MapShowcase.tsx +446 -0
- package/components/examples/RouteMapExamples.tsx +329 -0
- package/components/examples/SimpleFilterableMap.tsx +192 -0
- package/components/examples/index.ts +52 -0
- package/components/figma/ImageWithFallback.tsx +27 -0
- package/components/index.ts +44 -0
- package/components/media/AudioPlayer.tsx +278 -0
- package/components/media/FloatingMediaWrapper.tsx +166 -0
- package/components/media/VideoPlayer.tsx +285 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +159 -0
- package/components/ui/alert.tsx +91 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +65 -0
- package/components/ui/badge.tsx +55 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button.tsx +78 -0
- package/components/ui/calendar.tsx +235 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +353 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +177 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +138 -0
- package/components/ui/drawer.tsx +134 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/empty.tsx +90 -0
- package/components/ui/file-upload.tsx +152 -0
- package/components/ui/form.tsx +195 -0
- package/components/ui/google-maps-loader.tsx +379 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/index.ts +242 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +38 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/map-config.ts +12 -0
- package/components/ui/map-layers.tsx +129 -0
- package/components/ui/map.exports.ts +31 -0
- package/components/ui/map.tsx +412 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/navigation-menu.tsx +162 -0
- package/components/ui/notification-badge.tsx +61 -0
- package/components/ui/page-header.tsx +229 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +56 -0
- package/components/ui/rating.tsx +102 -0
- package/components/ui/resizable.tsx +405 -0
- package/components/ui/route-map.tsx +246 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/search.tsx +70 -0
- package/components/ui/select.tsx +176 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/simple-map.tsx +92 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/slider.tsx +58 -0
- package/components/ui/sonner.tsx +77 -0
- package/components/ui/stats-card.tsx +84 -0
- package/components/ui/stepper.tsx +126 -0
- package/components/ui/switch.tsx +34 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +66 -0
- package/components/ui/textarea.tsx +26 -0
- package/components/ui/timeline.tsx +140 -0
- package/components/ui/toggle-group.tsx +71 -0
- package/components/ui/toggle.tsx +46 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/tree-view.tsx +123 -0
- package/components/ui/use-mobile.ts +24 -0
- package/components/ui/utils.ts +6 -0
- package/components/ui/xertica-assistant.tsx +1420 -0
- package/contexts/ApiKeyContext.tsx +123 -0
- package/contexts/AssistenteContext.tsx +118 -0
- package/contexts/BrandColorsContext.tsx +551 -0
- package/contexts/LanguageContext.tsx +36 -0
- package/contexts/ThemeContext.tsx +85 -0
- package/dist/cli.js +20922 -0
- package/eslint.config.js +41 -0
- package/guidelines/Guidelines.md +61 -0
- package/hooks/useTheme.ts +4 -0
- package/imports/Podcast.tsx +389 -0
- package/imports/XerticaAi.tsx +46 -0
- package/imports/XerticaX.tsx +20 -0
- package/imports/svg-aueiaqngck.ts +11 -0
- package/imports/svg-v9krss1ozd.ts +16 -0
- package/imports/svg-vhrdofe3qe.ts +5 -0
- package/index.css +4448 -0
- package/index.html +14 -0
- package/main.tsx +10 -0
- package/package.json +119 -0
- package/postcss.config.js +6 -0
- package/routes.tsx +33 -0
- package/styles/globals.css +15 -0
- package/styles/xertica/app-overrides/chat.css +61 -0
- package/styles/xertica/app-overrides/scrollbar.css +33 -0
- package/styles/xertica/base.css +70 -0
- package/styles/xertica/integrations/google-maps.css +76 -0
- package/styles/xertica/integrations/sonner.css +73 -0
- package/styles/xertica/theme-map.css +88 -0
- package/styles/xertica/tokens.css +190 -0
- package/tsconfig.json +31 -0
- package/tsconfig.node.json +10 -0
- package/utils/gemini.ts +140 -0
- package/vite-env.d.ts +12 -0
- package/vite.config.ts +36 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
|
5
|
+
|
|
6
|
+
import { cn } from "./utils"
|
|
7
|
+
|
|
8
|
+
const RadioGroup = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
|
11
|
+
>(({ className, ...props }, ref) => (
|
|
12
|
+
<RadioGroupPrimitive.Root
|
|
13
|
+
className={cn("grid gap-4", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
ref={ref}
|
|
16
|
+
/>
|
|
17
|
+
))
|
|
18
|
+
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
|
19
|
+
|
|
20
|
+
const RadioGroupItem = React.forwardRef<
|
|
21
|
+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
22
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
|
23
|
+
>(({ className, ...props }, ref) => (
|
|
24
|
+
<RadioGroupPrimitive.Item
|
|
25
|
+
ref={ref}
|
|
26
|
+
data-slot="radio-group-item"
|
|
27
|
+
className={cn(
|
|
28
|
+
// Base styles - Material UI inspired
|
|
29
|
+
"relative aspect-square size-5 shrink-0 rounded-full border-2 transition-all duration-200 outline-none cursor-pointer",
|
|
30
|
+
// Default state - sempre com contorno usando variáveis CSS
|
|
31
|
+
"border-muted-foreground bg-background",
|
|
32
|
+
// Hover state - usa variável primary
|
|
33
|
+
"hover:border-primary hover:shadow-md",
|
|
34
|
+
// Focus state - usa variável primary
|
|
35
|
+
"focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
|
|
36
|
+
// Checked state - mantém contorno com primary
|
|
37
|
+
"data-[state=checked]:border-primary data-[state=checked]:bg-background",
|
|
38
|
+
// Disabled state
|
|
39
|
+
"disabled:cursor-not-allowed disabled:opacity-50 disabled:border-muted",
|
|
40
|
+
// Invalid state - usa variável destructive
|
|
41
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
<RadioGroupPrimitive.Indicator
|
|
47
|
+
data-slot="radio-group-indicator"
|
|
48
|
+
className="flex items-center justify-center"
|
|
49
|
+
>
|
|
50
|
+
<div className="size-2.5 rounded-full bg-primary transition-all duration-200" />
|
|
51
|
+
</RadioGroupPrimitive.Indicator>
|
|
52
|
+
</RadioGroupPrimitive.Item>
|
|
53
|
+
))
|
|
54
|
+
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
|
55
|
+
|
|
56
|
+
export { RadioGroup, RadioGroupItem }
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Star } from "lucide-react";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
interface RatingProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
6
|
+
value?: number;
|
|
7
|
+
onChange?: (value: number) => void;
|
|
8
|
+
max?: number;
|
|
9
|
+
readonly?: boolean;
|
|
10
|
+
size?: "sm" | "md" | "lg";
|
|
11
|
+
showValue?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const Rating = React.forwardRef<HTMLDivElement, RatingProps>(
|
|
15
|
+
({
|
|
16
|
+
className,
|
|
17
|
+
value = 0,
|
|
18
|
+
onChange,
|
|
19
|
+
max = 5,
|
|
20
|
+
readonly = false,
|
|
21
|
+
size = "md",
|
|
22
|
+
showValue = false,
|
|
23
|
+
...props
|
|
24
|
+
}, ref) => {
|
|
25
|
+
const [hoverValue, setHoverValue] = React.useState<number | null>(null);
|
|
26
|
+
|
|
27
|
+
const sizeStyles = {
|
|
28
|
+
sm: "h-4 w-4",
|
|
29
|
+
md: "h-5 w-5",
|
|
30
|
+
lg: "h-6 w-6",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleClick = (rating: number) => {
|
|
34
|
+
if (!readonly && onChange) {
|
|
35
|
+
onChange(rating);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleMouseEnter = (rating: number) => {
|
|
40
|
+
if (!readonly) {
|
|
41
|
+
setHoverValue(rating);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleMouseLeave = () => {
|
|
46
|
+
if (!readonly) {
|
|
47
|
+
setHoverValue(null);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const displayValue = hoverValue ?? value;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
ref={ref}
|
|
56
|
+
className={cn("flex items-center gap-1", className)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
<div className="flex items-center gap-0.5">
|
|
60
|
+
{Array.from({ length: max }, (_, index) => {
|
|
61
|
+
const rating = index + 1;
|
|
62
|
+
const isFilled = rating <= displayValue;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
key={index}
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={() => handleClick(rating)}
|
|
69
|
+
onMouseEnter={() => handleMouseEnter(rating)}
|
|
70
|
+
onMouseLeave={handleMouseLeave}
|
|
71
|
+
disabled={readonly}
|
|
72
|
+
className={cn(
|
|
73
|
+
"transition-colors focus:outline-none",
|
|
74
|
+
!readonly && "cursor-pointer hover:scale-110",
|
|
75
|
+
readonly && "cursor-default"
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
<Star
|
|
79
|
+
className={cn(
|
|
80
|
+
sizeStyles[size],
|
|
81
|
+
isFilled
|
|
82
|
+
? "fill-[rgb(245,158,11)] text-[rgb(245,158,11)]"
|
|
83
|
+
: "fill-none text-muted-foreground"
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
</button>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
</div>
|
|
90
|
+
{showValue && (
|
|
91
|
+
<span className="ml-2 text-muted-foreground">
|
|
92
|
+
{value.toFixed(1)}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
Rating.displayName = "Rating";
|
|
100
|
+
|
|
101
|
+
export { Rating };
|
|
102
|
+
export type { RatingProps };
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { GripVerticalIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
// --- Types ---
|
|
8
|
+
|
|
9
|
+
type PanelData = {
|
|
10
|
+
id: string;
|
|
11
|
+
ref: React.MutableRefObject<HTMLDivElement | null>;
|
|
12
|
+
defaultSize?: number;
|
|
13
|
+
minSize?: number;
|
|
14
|
+
maxSize?: number;
|
|
15
|
+
collapsible?: boolean;
|
|
16
|
+
onCollapse?: () => void;
|
|
17
|
+
onExpand?: () => void;
|
|
18
|
+
onResize?: (size: number) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type HandleData = {
|
|
22
|
+
id: string;
|
|
23
|
+
ref: React.MutableRefObject<HTMLDivElement | null>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ResizableContextType = {
|
|
27
|
+
direction: "horizontal" | "vertical";
|
|
28
|
+
registerPanel: (data: PanelData) => void;
|
|
29
|
+
unregisterPanel: (id: string) => void;
|
|
30
|
+
registerHandle: (data: HandleData) => void;
|
|
31
|
+
unregisterHandle: (id: string) => void;
|
|
32
|
+
isDragging: boolean;
|
|
33
|
+
startDragging: (handleId: string, event: React.MouseEvent | React.TouchEvent) => void;
|
|
34
|
+
getPanelStyle: (id: string) => React.CSSProperties;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ResizableContext = React.createContext<ResizableContextType | null>(null);
|
|
38
|
+
|
|
39
|
+
// --- Components ---
|
|
40
|
+
|
|
41
|
+
const ResizablePanelGroup = ({
|
|
42
|
+
children,
|
|
43
|
+
className,
|
|
44
|
+
direction = "horizontal",
|
|
45
|
+
id,
|
|
46
|
+
autoSaveId,
|
|
47
|
+
storage,
|
|
48
|
+
onLayout,
|
|
49
|
+
...props
|
|
50
|
+
}: {
|
|
51
|
+
children: React.ReactNode;
|
|
52
|
+
className?: string;
|
|
53
|
+
direction?: "horizontal" | "vertical";
|
|
54
|
+
id?: string;
|
|
55
|
+
autoSaveId?: string;
|
|
56
|
+
storage?: any;
|
|
57
|
+
onLayout?: (sizes: number[]) => void;
|
|
58
|
+
} & React.HTMLAttributes<HTMLDivElement>) => {
|
|
59
|
+
const [panels, setPanels] = React.useState<Map<string, PanelData>>(new Map());
|
|
60
|
+
const [handles, setHandles] = React.useState<Map<string, HandleData>>(new Map());
|
|
61
|
+
const [sizes, setSizes] = React.useState<Map<string, number>>(new Map());
|
|
62
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
63
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
64
|
+
|
|
65
|
+
// Sorting helpers
|
|
66
|
+
const getSortedItems = React.useCallback(<T extends { ref: React.MutableRefObject<HTMLDivElement | null> }>(
|
|
67
|
+
items: Map<string, T>
|
|
68
|
+
) => {
|
|
69
|
+
if (!containerRef.current) return [];
|
|
70
|
+
return Array.from(items.values()).sort((a, b) => {
|
|
71
|
+
if (!a.ref.current || !b.ref.current) return 0;
|
|
72
|
+
const position = a.ref.current.compareDocumentPosition(b.ref.current);
|
|
73
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
|
|
74
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
|
|
75
|
+
return 0;
|
|
76
|
+
});
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
// Initialize layout
|
|
80
|
+
React.useEffect(() => {
|
|
81
|
+
const sortedPanels = getSortedItems(panels);
|
|
82
|
+
if (sortedPanels.length === 0) return;
|
|
83
|
+
|
|
84
|
+
// Check if we need to initialize sizes
|
|
85
|
+
const uninitialized = sortedPanels.some(p => !sizes.has(p.id));
|
|
86
|
+
if (uninitialized) {
|
|
87
|
+
const newSizes = new Map(sizes);
|
|
88
|
+
const remainingSpace = 100;
|
|
89
|
+
const defaultSizeCount = sortedPanels.filter(p => p.defaultSize).length;
|
|
90
|
+
|
|
91
|
+
let usedSpace = 0;
|
|
92
|
+
sortedPanels.forEach(p => {
|
|
93
|
+
if (p.defaultSize) usedSpace += p.defaultSize;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const spaceForOthers = Math.max(0, remainingSpace - usedSpace);
|
|
97
|
+
const countOthers = sortedPanels.length - defaultSizeCount;
|
|
98
|
+
const sizeForOthers = countOthers > 0 ? spaceForOthers / countOthers : 0;
|
|
99
|
+
|
|
100
|
+
sortedPanels.forEach(p => {
|
|
101
|
+
if (!newSizes.has(p.id)) {
|
|
102
|
+
newSizes.set(p.id, p.defaultSize ?? sizeForOthers);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
setSizes(newSizes);
|
|
106
|
+
}
|
|
107
|
+
}, [panels, sizes, getSortedItems]);
|
|
108
|
+
|
|
109
|
+
const registerPanel = React.useCallback((data: PanelData) => {
|
|
110
|
+
setPanels(prev => {
|
|
111
|
+
const next = new Map(prev);
|
|
112
|
+
next.set(data.id, data);
|
|
113
|
+
return next;
|
|
114
|
+
});
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
const unregisterPanel = React.useCallback((id: string) => {
|
|
118
|
+
setPanels(prev => {
|
|
119
|
+
const next = new Map(prev);
|
|
120
|
+
next.delete(id);
|
|
121
|
+
return next;
|
|
122
|
+
});
|
|
123
|
+
setSizes(prev => {
|
|
124
|
+
const next = new Map(prev);
|
|
125
|
+
next.delete(id);
|
|
126
|
+
return next;
|
|
127
|
+
});
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
const registerHandle = React.useCallback((data: HandleData) => {
|
|
131
|
+
setHandles(prev => {
|
|
132
|
+
const next = new Map(prev);
|
|
133
|
+
next.set(data.id, data);
|
|
134
|
+
return next;
|
|
135
|
+
});
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
const unregisterHandle = React.useCallback((id: string) => {
|
|
139
|
+
setHandles(prev => {
|
|
140
|
+
const next = new Map(prev);
|
|
141
|
+
next.delete(id);
|
|
142
|
+
return next;
|
|
143
|
+
});
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
const startDragging = React.useCallback((handleId: string, event: React.MouseEvent | React.TouchEvent) => {
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
setIsDragging(true);
|
|
149
|
+
|
|
150
|
+
const sortedPanels = getSortedItems(panels);
|
|
151
|
+
const sortedHandles = getSortedItems(handles);
|
|
152
|
+
|
|
153
|
+
const handleIndex = sortedHandles.findIndex(h => h.id === handleId);
|
|
154
|
+
if (handleIndex === -1) return;
|
|
155
|
+
|
|
156
|
+
// Handle i usually sits between Panel i and Panel i+1
|
|
157
|
+
const leftPanel = sortedPanels[handleIndex];
|
|
158
|
+
const rightPanel = sortedPanels[handleIndex + 1];
|
|
159
|
+
|
|
160
|
+
if (!leftPanel || !rightPanel) return;
|
|
161
|
+
|
|
162
|
+
const startX = 'touches' in event ? event.touches[0].clientX : event.clientX;
|
|
163
|
+
const startY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
|
164
|
+
|
|
165
|
+
const startSizeLeft = sizes.get(leftPanel.id) || 0;
|
|
166
|
+
const startSizeRight = sizes.get(rightPanel.id) || 0;
|
|
167
|
+
|
|
168
|
+
const containerSize = direction === 'horizontal'
|
|
169
|
+
? containerRef.current?.offsetWidth || 1
|
|
170
|
+
: containerRef.current?.offsetHeight || 1;
|
|
171
|
+
|
|
172
|
+
const onMove = (e: MouseEvent | TouchEvent) => {
|
|
173
|
+
const currentX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
174
|
+
const currentY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
175
|
+
|
|
176
|
+
const deltaPixels = direction === 'horizontal' ? currentX - startX : currentY - startY;
|
|
177
|
+
const deltaPercent = (deltaPixels / containerSize) * 100;
|
|
178
|
+
|
|
179
|
+
// Calculate tentative new sizes
|
|
180
|
+
let finalLeft = startSizeLeft + deltaPercent;
|
|
181
|
+
let finalRight = startSizeRight - deltaPercent;
|
|
182
|
+
|
|
183
|
+
// Apply min constraints
|
|
184
|
+
if (leftPanel.minSize !== undefined && finalLeft < leftPanel.minSize) {
|
|
185
|
+
const diff = leftPanel.minSize - finalLeft;
|
|
186
|
+
finalLeft = leftPanel.minSize;
|
|
187
|
+
finalRight -= diff;
|
|
188
|
+
}
|
|
189
|
+
if (rightPanel.minSize !== undefined && finalRight < rightPanel.minSize) {
|
|
190
|
+
const diff = rightPanel.minSize - finalRight;
|
|
191
|
+
finalRight = rightPanel.minSize;
|
|
192
|
+
finalLeft -= diff;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Apply max constraints
|
|
196
|
+
if (leftPanel.maxSize !== undefined && finalLeft > leftPanel.maxSize) {
|
|
197
|
+
const diff = finalLeft - leftPanel.maxSize;
|
|
198
|
+
finalLeft = leftPanel.maxSize;
|
|
199
|
+
finalRight += diff;
|
|
200
|
+
}
|
|
201
|
+
if (rightPanel.maxSize !== undefined && finalRight > rightPanel.maxSize) {
|
|
202
|
+
const diff = finalRight - rightPanel.maxSize;
|
|
203
|
+
finalRight = rightPanel.maxSize;
|
|
204
|
+
finalLeft -= diff;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Hard clamp to 0-100 just in case
|
|
208
|
+
finalLeft = Math.max(0, Math.min(100, finalLeft));
|
|
209
|
+
finalRight = Math.max(0, Math.min(100, finalRight));
|
|
210
|
+
|
|
211
|
+
setSizes(prev => {
|
|
212
|
+
const next = new Map(prev);
|
|
213
|
+
next.set(leftPanel.id, finalLeft);
|
|
214
|
+
next.set(rightPanel.id, finalRight);
|
|
215
|
+
return next;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Notify panels
|
|
219
|
+
leftPanel.onResize?.(finalLeft);
|
|
220
|
+
rightPanel.onResize?.(finalRight);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const onUp = () => {
|
|
224
|
+
setIsDragging(false);
|
|
225
|
+
window.removeEventListener('mousemove', onMove);
|
|
226
|
+
window.removeEventListener('mouseup', onUp);
|
|
227
|
+
window.removeEventListener('touchmove', onMove);
|
|
228
|
+
window.removeEventListener('touchend', onUp);
|
|
229
|
+
|
|
230
|
+
// Trigger onLayout callback
|
|
231
|
+
if (onLayout) {
|
|
232
|
+
onLayout(sortedPanels.map(p => sizes.get(p.id) || 0));
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
window.addEventListener('mousemove', onMove);
|
|
237
|
+
window.addEventListener('mouseup', onUp);
|
|
238
|
+
window.addEventListener('touchmove', onMove);
|
|
239
|
+
window.addEventListener('touchend', onUp);
|
|
240
|
+
}, [panels, handles, sizes, direction, getSortedItems, onLayout]);
|
|
241
|
+
|
|
242
|
+
const getPanelStyle = React.useCallback((id: string) => {
|
|
243
|
+
const size = sizes.get(id);
|
|
244
|
+
if (size === undefined) return { flex: '1 1 0%', overflow: 'hidden' };
|
|
245
|
+
return { flex: `${size} 1 0%`, overflow: 'hidden' };
|
|
246
|
+
}, [sizes]);
|
|
247
|
+
|
|
248
|
+
const contextValue = React.useMemo(() => ({
|
|
249
|
+
direction,
|
|
250
|
+
registerPanel,
|
|
251
|
+
unregisterPanel,
|
|
252
|
+
registerHandle,
|
|
253
|
+
unregisterHandle,
|
|
254
|
+
isDragging,
|
|
255
|
+
startDragging,
|
|
256
|
+
getPanelStyle
|
|
257
|
+
}), [direction, registerPanel, unregisterPanel, registerHandle, unregisterHandle, isDragging, startDragging, getPanelStyle]);
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<ResizableContext.Provider value={contextValue}>
|
|
261
|
+
<div
|
|
262
|
+
ref={containerRef}
|
|
263
|
+
data-slot="resizable-panel-group"
|
|
264
|
+
data-panel-group-direction={direction}
|
|
265
|
+
className={cn(
|
|
266
|
+
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
|
267
|
+
className
|
|
268
|
+
)}
|
|
269
|
+
{...props}
|
|
270
|
+
>
|
|
271
|
+
{children}
|
|
272
|
+
</div>
|
|
273
|
+
</ResizableContext.Provider>
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const ResizablePanel = ({
|
|
278
|
+
className,
|
|
279
|
+
defaultSize,
|
|
280
|
+
minSize = 0,
|
|
281
|
+
maxSize = 100,
|
|
282
|
+
collapsible,
|
|
283
|
+
collapsedSize,
|
|
284
|
+
onCollapse,
|
|
285
|
+
onExpand,
|
|
286
|
+
onResize,
|
|
287
|
+
order,
|
|
288
|
+
tagName,
|
|
289
|
+
id: propId,
|
|
290
|
+
children,
|
|
291
|
+
...props
|
|
292
|
+
}: {
|
|
293
|
+
defaultSize?: number;
|
|
294
|
+
minSize?: number;
|
|
295
|
+
maxSize?: number;
|
|
296
|
+
collapsible?: boolean;
|
|
297
|
+
collapsedSize?: number;
|
|
298
|
+
onCollapse?: () => void;
|
|
299
|
+
onExpand?: () => void;
|
|
300
|
+
onResize?: (size: number) => void;
|
|
301
|
+
order?: number;
|
|
302
|
+
tagName?: string;
|
|
303
|
+
} & React.HTMLAttributes<HTMLDivElement>) => {
|
|
304
|
+
const context = React.useContext(ResizableContext);
|
|
305
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
306
|
+
const [generatedId] = React.useState(() => Math.random().toString(36).substr(2, 9));
|
|
307
|
+
const id = propId || generatedId;
|
|
308
|
+
|
|
309
|
+
// We destructure only stable functions for the dependency array to avoid infinite loops
|
|
310
|
+
const registerPanel = context?.registerPanel;
|
|
311
|
+
const unregisterPanel = context?.unregisterPanel;
|
|
312
|
+
|
|
313
|
+
React.useLayoutEffect(() => {
|
|
314
|
+
if (!registerPanel || !unregisterPanel) return;
|
|
315
|
+
registerPanel({
|
|
316
|
+
id,
|
|
317
|
+
ref,
|
|
318
|
+
defaultSize,
|
|
319
|
+
minSize,
|
|
320
|
+
maxSize,
|
|
321
|
+
collapsible,
|
|
322
|
+
onCollapse,
|
|
323
|
+
onExpand,
|
|
324
|
+
onResize
|
|
325
|
+
});
|
|
326
|
+
return () => unregisterPanel(id);
|
|
327
|
+
}, [registerPanel, unregisterPanel, id, defaultSize, minSize, maxSize, collapsible, onCollapse, onExpand, onResize]);
|
|
328
|
+
|
|
329
|
+
if (!context) {
|
|
330
|
+
return <div className={cn("flex-1", className)} {...props}>{children}</div>;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<div
|
|
335
|
+
ref={ref}
|
|
336
|
+
data-slot="resizable-panel"
|
|
337
|
+
className={cn("relative transition-[flex-grow] duration-0", className)}
|
|
338
|
+
style={context.getPanelStyle(id)}
|
|
339
|
+
{...props}
|
|
340
|
+
>
|
|
341
|
+
{children}
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const ResizableHandle = ({
|
|
347
|
+
withHandle,
|
|
348
|
+
className,
|
|
349
|
+
id: propId,
|
|
350
|
+
tagName,
|
|
351
|
+
...props
|
|
352
|
+
}: {
|
|
353
|
+
withHandle?: boolean;
|
|
354
|
+
tagName?: string;
|
|
355
|
+
} & React.HTMLAttributes<HTMLDivElement>) => {
|
|
356
|
+
const context = React.useContext(ResizableContext);
|
|
357
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
358
|
+
const [generatedId] = React.useState(() => Math.random().toString(36).substr(2, 9));
|
|
359
|
+
const id = propId || generatedId;
|
|
360
|
+
|
|
361
|
+
// We destructure only stable functions for the dependency array
|
|
362
|
+
const registerHandle = context?.registerHandle;
|
|
363
|
+
const unregisterHandle = context?.unregisterHandle;
|
|
364
|
+
|
|
365
|
+
React.useLayoutEffect(() => {
|
|
366
|
+
if (!registerHandle || !unregisterHandle) return;
|
|
367
|
+
registerHandle({ id, ref });
|
|
368
|
+
return () => unregisterHandle(id);
|
|
369
|
+
}, [registerHandle, unregisterHandle, id]);
|
|
370
|
+
|
|
371
|
+
const handleMouseDown = (e: React.MouseEvent | React.TouchEvent) => {
|
|
372
|
+
if (context) {
|
|
373
|
+
context.startDragging(id, e);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
if (!context) return null;
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<div
|
|
381
|
+
ref={ref}
|
|
382
|
+
data-slot="resizable-handle"
|
|
383
|
+
className={cn(
|
|
384
|
+
"bg-border relative flex items-center justify-center focus-visible:outline-hidden",
|
|
385
|
+
"touch-none select-none",
|
|
386
|
+
context.direction === "vertical"
|
|
387
|
+
? "h-px w-full cursor-row-resize after:left-0 after:h-1 after:w-full after:-translate-y-1/2 hover:after:h-4"
|
|
388
|
+
: "w-px h-full cursor-col-resize after:top-0 after:w-1 after:h-full after:-translate-x-1/2 hover:after:w-4",
|
|
389
|
+
"after:absolute after:z-10",
|
|
390
|
+
className
|
|
391
|
+
)}
|
|
392
|
+
onMouseDown={handleMouseDown}
|
|
393
|
+
onTouchStart={handleMouseDown}
|
|
394
|
+
{...props}
|
|
395
|
+
>
|
|
396
|
+
{withHandle && (
|
|
397
|
+
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
|
398
|
+
<GripVerticalIcon className="size-2.5" />
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
402
|
+
);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|