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,140 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "./utils";
|
|
3
|
+
|
|
4
|
+
const Timeline = React.forwardRef<
|
|
5
|
+
HTMLOListElement,
|
|
6
|
+
React.HTMLAttributes<HTMLOListElement>
|
|
7
|
+
>(({ className, ...props }, ref) => (
|
|
8
|
+
<ol
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn("relative border-l border-border", className)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
));
|
|
14
|
+
Timeline.displayName = "Timeline";
|
|
15
|
+
|
|
16
|
+
const TimelineItem = React.forwardRef<
|
|
17
|
+
HTMLLIElement,
|
|
18
|
+
React.HTMLAttributes<HTMLLIElement>
|
|
19
|
+
>(({ className, ...props }, ref) => (
|
|
20
|
+
<li
|
|
21
|
+
ref={ref}
|
|
22
|
+
className={cn("mb-10 ml-6", className)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
));
|
|
26
|
+
TimelineItem.displayName = "TimelineItem";
|
|
27
|
+
|
|
28
|
+
const TimelineDot = React.forwardRef<
|
|
29
|
+
HTMLDivElement,
|
|
30
|
+
React.HTMLAttributes<HTMLDivElement> & {
|
|
31
|
+
variant?: "default" | "primary" | "success" | "warning" | "destructive";
|
|
32
|
+
icon?: React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
>(({ className, variant = "default", icon, children, ...props }, ref) => {
|
|
35
|
+
const variantStyles = {
|
|
36
|
+
default: "bg-muted border-border",
|
|
37
|
+
primary: "bg-primary border-primary",
|
|
38
|
+
success: "bg-[rgb(5,150,105)] border-[rgb(5,150,105)]",
|
|
39
|
+
warning: "bg-[rgb(245,158,11)] border-[rgb(245,158,11)]",
|
|
40
|
+
destructive: "bg-destructive border-destructive",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const iconColorStyles = {
|
|
44
|
+
default: "text-muted-foreground",
|
|
45
|
+
primary: "text-primary-foreground",
|
|
46
|
+
success: "text-white",
|
|
47
|
+
warning: "text-white",
|
|
48
|
+
destructive: "text-destructive-foreground",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Se tem ícone, usar tamanho maior
|
|
52
|
+
if (icon || children) {
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
ref={ref}
|
|
56
|
+
className={cn(
|
|
57
|
+
"absolute -left-[17px] mt-0.5 h-8 w-8 rounded-full border-2 flex items-center justify-center",
|
|
58
|
+
variantStyles[variant],
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
<span className={cn("h-4 w-4", iconColorStyles[variant])}>
|
|
64
|
+
{icon || children}
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={cn(
|
|
74
|
+
"absolute -left-[9px] mt-1.5 h-4 w-4 rounded-full border-2",
|
|
75
|
+
variantStyles[variant],
|
|
76
|
+
className
|
|
77
|
+
)}
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
TimelineDot.displayName = "TimelineDot";
|
|
83
|
+
|
|
84
|
+
const TimelineContent = React.forwardRef<
|
|
85
|
+
HTMLDivElement,
|
|
86
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
87
|
+
>(({ className, ...props }, ref) => (
|
|
88
|
+
<div
|
|
89
|
+
ref={ref}
|
|
90
|
+
className={cn("", className)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
));
|
|
94
|
+
TimelineContent.displayName = "TimelineContent";
|
|
95
|
+
|
|
96
|
+
const TimelineHeading = React.forwardRef<
|
|
97
|
+
HTMLHeadingElement,
|
|
98
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
99
|
+
>(({ className, ...props }, ref) => (
|
|
100
|
+
<h3
|
|
101
|
+
ref={ref}
|
|
102
|
+
className={cn("mb-1 text-foreground", className)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
));
|
|
106
|
+
TimelineHeading.displayName = "TimelineHeading";
|
|
107
|
+
|
|
108
|
+
const TimelineTime = React.forwardRef<
|
|
109
|
+
HTMLTimeElement,
|
|
110
|
+
React.HTMLAttributes<HTMLTimeElement>
|
|
111
|
+
>(({ className, ...props }, ref) => (
|
|
112
|
+
<time
|
|
113
|
+
ref={ref}
|
|
114
|
+
className={cn("mb-2 block text-muted-foreground", className)}
|
|
115
|
+
{...props}
|
|
116
|
+
/>
|
|
117
|
+
));
|
|
118
|
+
TimelineTime.displayName = "TimelineTime";
|
|
119
|
+
|
|
120
|
+
const TimelineDescription = React.forwardRef<
|
|
121
|
+
HTMLParagraphElement,
|
|
122
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
123
|
+
>(({ className, ...props }, ref) => (
|
|
124
|
+
<p
|
|
125
|
+
ref={ref}
|
|
126
|
+
className={cn("text-muted-foreground", className)}
|
|
127
|
+
{...props}
|
|
128
|
+
/>
|
|
129
|
+
));
|
|
130
|
+
TimelineDescription.displayName = "TimelineDescription";
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
Timeline,
|
|
134
|
+
TimelineItem,
|
|
135
|
+
TimelineDot,
|
|
136
|
+
TimelineContent,
|
|
137
|
+
TimelineHeading,
|
|
138
|
+
TimelineTime,
|
|
139
|
+
TimelineDescription,
|
|
140
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
|
5
|
+
import { type VariantProps } from "class-variance-authority"
|
|
6
|
+
|
|
7
|
+
import { cn } from "./utils"
|
|
8
|
+
import { toggleVariants } from "./toggle"
|
|
9
|
+
|
|
10
|
+
const ToggleGroupContext = React.createContext<
|
|
11
|
+
VariantProps<typeof toggleVariants>
|
|
12
|
+
>({
|
|
13
|
+
size: "default",
|
|
14
|
+
variant: "default",
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const ToggleGroup = React.forwardRef<
|
|
18
|
+
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
|
19
|
+
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
|
20
|
+
VariantProps<typeof toggleVariants>
|
|
21
|
+
>(({ className, variant, size, children, ...props }, ref) => (
|
|
22
|
+
<ToggleGroupPrimitive.Root
|
|
23
|
+
ref={ref}
|
|
24
|
+
data-slot="toggle-group"
|
|
25
|
+
data-variant={variant}
|
|
26
|
+
data-size={size}
|
|
27
|
+
className={cn(
|
|
28
|
+
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
<ToggleGroupContext.Provider value={{ variant, size }}>
|
|
34
|
+
{children}
|
|
35
|
+
</ToggleGroupContext.Provider>
|
|
36
|
+
</ToggleGroupPrimitive.Root>
|
|
37
|
+
))
|
|
38
|
+
|
|
39
|
+
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
|
|
40
|
+
|
|
41
|
+
const ToggleGroupItem = React.forwardRef<
|
|
42
|
+
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
|
43
|
+
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
|
44
|
+
VariantProps<typeof toggleVariants>
|
|
45
|
+
>(({ className, children, variant, size, ...props }, ref) => {
|
|
46
|
+
const context = React.useContext(ToggleGroupContext)
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<ToggleGroupPrimitive.Item
|
|
50
|
+
ref={ref}
|
|
51
|
+
data-slot="toggle-group-item"
|
|
52
|
+
data-variant={context.variant || variant}
|
|
53
|
+
data-size={context.size || size}
|
|
54
|
+
className={cn(
|
|
55
|
+
toggleVariants({
|
|
56
|
+
variant: context.variant || variant,
|
|
57
|
+
size: context.size || size,
|
|
58
|
+
}),
|
|
59
|
+
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
|
60
|
+
className
|
|
61
|
+
)}
|
|
62
|
+
{...props}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
</ToggleGroupPrimitive.Item>
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
|
|
70
|
+
|
|
71
|
+
export { ToggleGroup, ToggleGroupItem }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
6
|
+
|
|
7
|
+
import { cn } from "./utils"
|
|
8
|
+
|
|
9
|
+
const toggleVariants = cva(
|
|
10
|
+
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
|
11
|
+
{
|
|
12
|
+
variants: {
|
|
13
|
+
variant: {
|
|
14
|
+
default: "bg-transparent",
|
|
15
|
+
outline:
|
|
16
|
+
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
|
17
|
+
},
|
|
18
|
+
size: {
|
|
19
|
+
default: "h-9 px-2 min-w-9",
|
|
20
|
+
sm: "h-8 px-1.5 min-w-8",
|
|
21
|
+
lg: "h-10 px-2.5 min-w-10",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: "default",
|
|
26
|
+
size: "default",
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const Toggle = React.forwardRef<
|
|
32
|
+
React.ElementRef<typeof TogglePrimitive.Root>,
|
|
33
|
+
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
|
34
|
+
VariantProps<typeof toggleVariants>
|
|
35
|
+
>(({ className, variant, size, ...props }, ref) => (
|
|
36
|
+
<TogglePrimitive.Root
|
|
37
|
+
ref={ref}
|
|
38
|
+
data-slot="toggle"
|
|
39
|
+
className={cn(toggleVariants({ variant, size, className }))}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
))
|
|
43
|
+
|
|
44
|
+
Toggle.displayName = TogglePrimitive.Root.displayName
|
|
45
|
+
|
|
46
|
+
export { Toggle, toggleVariants }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
5
|
+
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
function TooltipProvider({
|
|
9
|
+
delayDuration = 0,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
12
|
+
return (
|
|
13
|
+
<TooltipPrimitive.Provider
|
|
14
|
+
data-slot="tooltip-provider"
|
|
15
|
+
delayDuration={delayDuration}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Tooltip({
|
|
22
|
+
...props
|
|
23
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
24
|
+
return (
|
|
25
|
+
<TooltipProvider>
|
|
26
|
+
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
27
|
+
</TooltipProvider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function TooltipTrigger({
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
34
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function TooltipContent({
|
|
38
|
+
className,
|
|
39
|
+
sideOffset = 0,
|
|
40
|
+
children,
|
|
41
|
+
...props
|
|
42
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
43
|
+
return (
|
|
44
|
+
<TooltipPrimitive.Portal>
|
|
45
|
+
<TooltipPrimitive.Content
|
|
46
|
+
data-slot="tooltip-content"
|
|
47
|
+
sideOffset={sideOffset}
|
|
48
|
+
className={cn(
|
|
49
|
+
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
|
56
|
+
</TooltipPrimitive.Content>
|
|
57
|
+
</TooltipPrimitive.Portal>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ChevronRight, ChevronDown } from "lucide-react";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export interface TreeNode {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
children?: TreeNode[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface TreeViewProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
13
|
+
data: TreeNode[];
|
|
14
|
+
onNodeClick?: (node: TreeNode) => void;
|
|
15
|
+
defaultExpanded?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const TreeView = React.forwardRef<HTMLDivElement, TreeViewProps>(
|
|
19
|
+
({ className, data, onNodeClick, defaultExpanded = [], ...props }, ref) => {
|
|
20
|
+
const [expanded, setExpanded] = React.useState<Set<string>>(
|
|
21
|
+
new Set(defaultExpanded)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const toggleExpand = (nodeId: string) => {
|
|
25
|
+
setExpanded((prev) => {
|
|
26
|
+
const newSet = new Set(prev);
|
|
27
|
+
if (newSet.has(nodeId)) {
|
|
28
|
+
newSet.delete(nodeId);
|
|
29
|
+
} else {
|
|
30
|
+
newSet.add(nodeId);
|
|
31
|
+
}
|
|
32
|
+
return newSet;
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
38
|
+
{data.map((node) => (
|
|
39
|
+
<TreeNodeComponent
|
|
40
|
+
key={node.id}
|
|
41
|
+
node={node}
|
|
42
|
+
level={0}
|
|
43
|
+
expanded={expanded}
|
|
44
|
+
onToggle={toggleExpand}
|
|
45
|
+
onNodeClick={onNodeClick}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
TreeView.displayName = "TreeView";
|
|
53
|
+
|
|
54
|
+
interface TreeNodeComponentProps {
|
|
55
|
+
node: TreeNode;
|
|
56
|
+
level: number;
|
|
57
|
+
expanded: Set<string>;
|
|
58
|
+
onToggle: (nodeId: string) => void;
|
|
59
|
+
onNodeClick?: (node: TreeNode) => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|
63
|
+
node,
|
|
64
|
+
level,
|
|
65
|
+
expanded,
|
|
66
|
+
onToggle,
|
|
67
|
+
onNodeClick,
|
|
68
|
+
}) => {
|
|
69
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
70
|
+
const isExpanded = expanded.has(node.id);
|
|
71
|
+
|
|
72
|
+
const handleClick = () => {
|
|
73
|
+
if (hasChildren) {
|
|
74
|
+
onToggle(node.id);
|
|
75
|
+
}
|
|
76
|
+
onNodeClick?.(node);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div>
|
|
81
|
+
<button
|
|
82
|
+
onClick={handleClick}
|
|
83
|
+
className={cn(
|
|
84
|
+
"flex w-full items-center gap-2 rounded-[var(--radius)] px-2 py-1.5 text-left transition-colors hover:bg-muted",
|
|
85
|
+
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
86
|
+
)}
|
|
87
|
+
style={{ paddingLeft: `${level * 1.5 + 0.5}rem` }}
|
|
88
|
+
>
|
|
89
|
+
{hasChildren && (
|
|
90
|
+
<span className="flex-shrink-0 text-muted-foreground">
|
|
91
|
+
{isExpanded ? (
|
|
92
|
+
<ChevronDown className="h-4 w-4" />
|
|
93
|
+
) : (
|
|
94
|
+
<ChevronRight className="h-4 w-4" />
|
|
95
|
+
)}
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
98
|
+
{!hasChildren && <span className="w-4" />}
|
|
99
|
+
{node.icon && (
|
|
100
|
+
<span className="flex-shrink-0 text-muted-foreground">{node.icon}</span>
|
|
101
|
+
)}
|
|
102
|
+
<span className="flex-1 text-foreground">{node.label}</span>
|
|
103
|
+
</button>
|
|
104
|
+
{hasChildren && isExpanded && (
|
|
105
|
+
<div>
|
|
106
|
+
{node.children!.map((child) => (
|
|
107
|
+
<TreeNodeComponent
|
|
108
|
+
key={child.id}
|
|
109
|
+
node={child}
|
|
110
|
+
level={level + 1}
|
|
111
|
+
expanded={expanded}
|
|
112
|
+
onToggle={onToggle}
|
|
113
|
+
onNodeClick={onNodeClick}
|
|
114
|
+
/>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export { TreeView };
|
|
123
|
+
export type { TreeViewProps, TreeNode };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768;
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
|
7
|
+
undefined,
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
12
|
+
const onChange = () => {
|
|
13
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
14
|
+
};
|
|
15
|
+
mql.addEventListener("change", onChange);
|
|
16
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
17
|
+
return () => mql.removeEventListener("change", onChange);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return !!isMobile;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Alias for compatibility
|
|
24
|
+
export const useMobile = useIsMobile;
|