@optilogic/core 1.0.0-beta.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/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/index.cjs +6003 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2310 -0
- package/dist/index.d.ts +2310 -0
- package/dist/index.js +5828 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +96 -0
- package/dist/tailwind-preset.cjs +106 -0
- package/dist/tailwind-preset.cjs.map +1 -0
- package/dist/tailwind-preset.d.cts +23 -0
- package/dist/tailwind-preset.d.ts +23 -0
- package/dist/tailwind-preset.js +101 -0
- package/dist/tailwind-preset.js.map +1 -0
- package/package.json +154 -0
- package/src/components/accordion.tsx +187 -0
- package/src/components/alert-dialog.tsx +143 -0
- package/src/components/autocomplete.tsx +271 -0
- package/src/components/badge.tsx +62 -0
- package/src/components/button.tsx +85 -0
- package/src/components/calendar.tsx +235 -0
- package/src/components/card.tsx +94 -0
- package/src/components/checkbox.tsx +77 -0
- package/src/components/chip.tsx +77 -0
- package/src/components/confirmation-modal.tsx +195 -0
- package/src/components/context-menu.tsx +406 -0
- package/src/components/copy-button.tsx +84 -0
- package/src/components/data-grid/DataGrid.tsx +1027 -0
- package/src/components/data-grid/components/CellEditor.tsx +346 -0
- package/src/components/data-grid/components/FilterPopover.tsx +459 -0
- package/src/components/data-grid/components/HeaderCell.tsx +207 -0
- package/src/components/data-grid/components/index.ts +14 -0
- package/src/components/data-grid/hooks/index.ts +28 -0
- package/src/components/data-grid/hooks/useColumnResize.ts +378 -0
- package/src/components/data-grid/hooks/useDataGridState.ts +346 -0
- package/src/components/data-grid/hooks/useKeyboardNavigation.ts +361 -0
- package/src/components/data-grid/index.ts +71 -0
- package/src/components/data-grid/types.ts +478 -0
- package/src/components/data-grid/utils/dataProcessing.ts +277 -0
- package/src/components/data-grid/utils/index.ts +12 -0
- package/src/components/date-picker.tsx +366 -0
- package/src/components/dropdown-menu.tsx +230 -0
- package/src/components/icon-button.tsx +157 -0
- package/src/components/input.tsx +40 -0
- package/src/components/label.tsx +37 -0
- package/src/components/loading-spinner.tsx +113 -0
- package/src/components/modal.tsx +207 -0
- package/src/components/popover.tsx +62 -0
- package/src/components/progress.tsx +41 -0
- package/src/components/resizable-panel.tsx +434 -0
- package/src/components/resize-handle.tsx +187 -0
- package/src/components/select.tsx +160 -0
- package/src/components/separator.tsx +50 -0
- package/src/components/skeleton.tsx +37 -0
- package/src/components/switch.tsx +59 -0
- package/src/components/table.tsx +136 -0
- package/src/components/tabs.tsx +102 -0
- package/src/components/textarea.tsx +36 -0
- package/src/components/theme-picker.tsx +245 -0
- package/src/components/toaster.tsx +84 -0
- package/src/components/tooltip.tsx +199 -0
- package/src/index.ts +318 -0
- package/src/styles.css +96 -0
- package/src/tailwind-preset.ts +129 -0
- package/src/theme/index.ts +41 -0
- package/src/theme/presets.ts +502 -0
- package/src/theme/types.ts +164 -0
- package/src/theme/utils.ts +309 -0
- package/src/utils/cn.ts +14 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThemePicker Component
|
|
3
|
+
*
|
|
4
|
+
* A polished theme selection component built with library primitives.
|
|
5
|
+
* Features:
|
|
6
|
+
* - Visual theme swatches showing color palette preview
|
|
7
|
+
* - Popover-based selection dropdown
|
|
8
|
+
* - Current theme indicator
|
|
9
|
+
* - Keyboard accessible
|
|
10
|
+
* - Smooth theme transitions
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import { Palette, Check } from "lucide-react";
|
|
15
|
+
import { cn } from "../utils/cn";
|
|
16
|
+
import { Popover, PopoverTrigger, PopoverContent } from "./popover";
|
|
17
|
+
import { IconButton } from "./icon-button";
|
|
18
|
+
import { Button } from "./button";
|
|
19
|
+
import { Tooltip } from "./tooltip";
|
|
20
|
+
import { ALL_THEMES, applyTheme, type Theme } from "../theme";
|
|
21
|
+
|
|
22
|
+
export interface ThemePickerProps {
|
|
23
|
+
/** Currently selected theme (controlled) */
|
|
24
|
+
value?: Theme;
|
|
25
|
+
/** Callback when theme changes */
|
|
26
|
+
onValueChange?: (theme: Theme) => void;
|
|
27
|
+
/** Available themes to choose from */
|
|
28
|
+
themes?: Theme[];
|
|
29
|
+
/** Trigger style variant */
|
|
30
|
+
triggerVariant?: "icon" | "button";
|
|
31
|
+
/** Size of the trigger */
|
|
32
|
+
triggerSize?: "sm" | "default" | "lg";
|
|
33
|
+
/** Whether to apply the theme to the DOM when selected */
|
|
34
|
+
applyOnSelect?: boolean;
|
|
35
|
+
/** Additional class name for the trigger */
|
|
36
|
+
className?: string;
|
|
37
|
+
/** Alignment of the popover */
|
|
38
|
+
align?: "start" | "center" | "end";
|
|
39
|
+
/** Side of the popover */
|
|
40
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Theme swatch component showing a preview of theme colors
|
|
45
|
+
*/
|
|
46
|
+
interface ThemeSwatchProps {
|
|
47
|
+
theme: Theme;
|
|
48
|
+
isSelected: boolean;
|
|
49
|
+
onClick: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ThemeSwatch({ theme, isSelected, onClick }: ThemeSwatchProps) {
|
|
53
|
+
return (
|
|
54
|
+
<Tooltip
|
|
55
|
+
content={
|
|
56
|
+
<div className="space-y-1">
|
|
57
|
+
<p className="font-medium">{theme.name}</p>
|
|
58
|
+
{theme.description && (
|
|
59
|
+
<p className="text-xs text-muted-foreground">{theme.description}</p>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
}
|
|
63
|
+
position="bottom"
|
|
64
|
+
delayDuration={300}
|
|
65
|
+
>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={onClick}
|
|
69
|
+
className={cn(
|
|
70
|
+
"relative flex flex-col items-center gap-1.5 p-2 rounded-lg transition-all",
|
|
71
|
+
"hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
72
|
+
isSelected && "bg-accent ring-2 ring-primary"
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
<div
|
|
76
|
+
className="relative w-12 h-8 rounded-md overflow-hidden border border-border shadow-sm"
|
|
77
|
+
style={{ backgroundColor: theme.background }}
|
|
78
|
+
>
|
|
79
|
+
<div
|
|
80
|
+
className="absolute bottom-0 left-0 right-0 h-2"
|
|
81
|
+
style={{ backgroundColor: theme.primary }}
|
|
82
|
+
/>
|
|
83
|
+
<div
|
|
84
|
+
className="absolute top-1 right-1 w-2 h-2 rounded-full"
|
|
85
|
+
style={{ backgroundColor: theme.accent }}
|
|
86
|
+
/>
|
|
87
|
+
<div
|
|
88
|
+
className="absolute top-1 left-1 w-4 h-1 rounded-full"
|
|
89
|
+
style={{ backgroundColor: theme.foreground }}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<span className="text-xs font-medium text-foreground truncate max-w-[60px]">
|
|
94
|
+
{theme.name.split(" ")[0]}
|
|
95
|
+
</span>
|
|
96
|
+
|
|
97
|
+
{isSelected && (
|
|
98
|
+
<div className="absolute -top-1 -right-1 w-4 h-4 bg-primary rounded-full flex items-center justify-center">
|
|
99
|
+
<Check className="w-2.5 h-2.5 text-primary-foreground" />
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</button>
|
|
103
|
+
</Tooltip>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* ThemePicker Component
|
|
109
|
+
*
|
|
110
|
+
* A theme selection dropdown that displays available themes as visual swatches.
|
|
111
|
+
*
|
|
112
|
+
* @example Basic usage
|
|
113
|
+
* ```tsx
|
|
114
|
+
* <ThemePicker onValueChange={(theme) => console.log(theme)} />
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @example Controlled with custom themes
|
|
118
|
+
* ```tsx
|
|
119
|
+
* const [theme, setTheme] = useState(OPTILOGIC_LEGACY_THEME);
|
|
120
|
+
* <ThemePicker
|
|
121
|
+
* value={theme}
|
|
122
|
+
* onValueChange={setTheme}
|
|
123
|
+
* themes={[OPTILOGIC_LEGACY_THEME, DARK_ELEGANT_THEME]}
|
|
124
|
+
* />
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export function ThemePicker({
|
|
128
|
+
value,
|
|
129
|
+
onValueChange,
|
|
130
|
+
themes = ALL_THEMES,
|
|
131
|
+
triggerVariant = "icon",
|
|
132
|
+
triggerSize = "default",
|
|
133
|
+
applyOnSelect = true,
|
|
134
|
+
className,
|
|
135
|
+
align = "end",
|
|
136
|
+
side = "bottom",
|
|
137
|
+
}: ThemePickerProps) {
|
|
138
|
+
const [open, setOpen] = React.useState(false);
|
|
139
|
+
const [selectedTheme, setSelectedTheme] = React.useState<Theme | undefined>(
|
|
140
|
+
value || themes[0]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
if (value) {
|
|
145
|
+
setSelectedTheme(value);
|
|
146
|
+
}
|
|
147
|
+
}, [value]);
|
|
148
|
+
|
|
149
|
+
const handleThemeSelect = (theme: Theme) => {
|
|
150
|
+
setSelectedTheme(theme);
|
|
151
|
+
|
|
152
|
+
if (applyOnSelect) {
|
|
153
|
+
applyTheme(theme);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
onValueChange?.(theme);
|
|
157
|
+
setOpen(false);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
161
|
+
if (!open) return;
|
|
162
|
+
|
|
163
|
+
const currentIndex = themes.findIndex((t) => t.id === selectedTheme?.id);
|
|
164
|
+
|
|
165
|
+
switch (e.key) {
|
|
166
|
+
case "ArrowRight":
|
|
167
|
+
case "ArrowDown":
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
const nextIndex = (currentIndex + 1) % themes.length;
|
|
170
|
+
handleThemeSelect(themes[nextIndex]);
|
|
171
|
+
break;
|
|
172
|
+
case "ArrowLeft":
|
|
173
|
+
case "ArrowUp":
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
const prevIndex = currentIndex <= 0 ? themes.length - 1 : currentIndex - 1;
|
|
176
|
+
handleThemeSelect(themes[prevIndex]);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
183
|
+
<PopoverTrigger asChild>
|
|
184
|
+
{triggerVariant === "icon" ? (
|
|
185
|
+
<IconButton
|
|
186
|
+
variant="ghost"
|
|
187
|
+
size={triggerSize}
|
|
188
|
+
aria-label="Select theme"
|
|
189
|
+
className={className}
|
|
190
|
+
>
|
|
191
|
+
<Palette className="w-4 h-4" />
|
|
192
|
+
</IconButton>
|
|
193
|
+
) : (
|
|
194
|
+
<Button
|
|
195
|
+
variant="outline"
|
|
196
|
+
size={triggerSize === "lg" ? "default" : triggerSize}
|
|
197
|
+
className={cn("gap-2", className)}
|
|
198
|
+
>
|
|
199
|
+
<Palette className="w-4 h-4" />
|
|
200
|
+
<span>{selectedTheme?.name || "Theme"}</span>
|
|
201
|
+
</Button>
|
|
202
|
+
)}
|
|
203
|
+
</PopoverTrigger>
|
|
204
|
+
|
|
205
|
+
<PopoverContent
|
|
206
|
+
align={align}
|
|
207
|
+
side={side}
|
|
208
|
+
className="w-auto p-3"
|
|
209
|
+
onKeyDown={handleKeyDown}
|
|
210
|
+
>
|
|
211
|
+
<div className="space-y-3">
|
|
212
|
+
<div className="px-1">
|
|
213
|
+
<h4 className="text-sm font-medium">Select Theme</h4>
|
|
214
|
+
<p className="text-xs text-muted-foreground">
|
|
215
|
+
Choose a color theme for the interface
|
|
216
|
+
</p>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div className="grid grid-cols-4 gap-1">
|
|
220
|
+
{themes.map((theme) => (
|
|
221
|
+
<ThemeSwatch
|
|
222
|
+
key={theme.id}
|
|
223
|
+
theme={theme}
|
|
224
|
+
isSelected={selectedTheme?.id === theme.id}
|
|
225
|
+
onClick={() => handleThemeSelect(theme)}
|
|
226
|
+
/>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{selectedTheme && (
|
|
231
|
+
<div className="pt-2 border-t border-border">
|
|
232
|
+
<div className="flex items-center gap-2 px-1">
|
|
233
|
+
<div
|
|
234
|
+
className="w-4 h-4 rounded-full border border-border"
|
|
235
|
+
style={{ backgroundColor: selectedTheme.primary }}
|
|
236
|
+
/>
|
|
237
|
+
<span className="text-sm font-medium">{selectedTheme.name}</span>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
</PopoverContent>
|
|
243
|
+
</Popover>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Toaster as Sonner } from "sonner";
|
|
2
|
+
|
|
3
|
+
export interface ToasterProps {
|
|
4
|
+
/**
|
|
5
|
+
* Position of the toaster
|
|
6
|
+
* @default "top-right"
|
|
7
|
+
*/
|
|
8
|
+
position?:
|
|
9
|
+
| "top-left"
|
|
10
|
+
| "top-center"
|
|
11
|
+
| "top-right"
|
|
12
|
+
| "bottom-left"
|
|
13
|
+
| "bottom-center"
|
|
14
|
+
| "bottom-right";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether to expand toasts on hover
|
|
18
|
+
* @default false
|
|
19
|
+
*/
|
|
20
|
+
expand?: boolean;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether to use rich colors
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
richColors?: boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether to show close button
|
|
30
|
+
* @default true
|
|
31
|
+
*/
|
|
32
|
+
closeButton?: boolean;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Theme mode
|
|
36
|
+
* @default "system"
|
|
37
|
+
*/
|
|
38
|
+
theme?: "light" | "dark" | "system";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Toaster component using Sonner
|
|
43
|
+
*
|
|
44
|
+
* Provides toast notifications throughout the app.
|
|
45
|
+
* Add this component once at the root of your app.
|
|
46
|
+
*
|
|
47
|
+
* Usage:
|
|
48
|
+
* import { toast } from "sonner";
|
|
49
|
+
* toast.success("Success!");
|
|
50
|
+
* toast.error("Error!");
|
|
51
|
+
* toast.info("Info!");
|
|
52
|
+
* toast.warning("Warning!");
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* // In your app root
|
|
56
|
+
* <Toaster />
|
|
57
|
+
*
|
|
58
|
+
* // Then anywhere in your app
|
|
59
|
+
* toast.success("Saved successfully!");
|
|
60
|
+
*/
|
|
61
|
+
export function Toaster({
|
|
62
|
+
position = "top-right",
|
|
63
|
+
expand = false,
|
|
64
|
+
richColors = true,
|
|
65
|
+
closeButton = true,
|
|
66
|
+
theme = "system",
|
|
67
|
+
}: ToasterProps = {}) {
|
|
68
|
+
return (
|
|
69
|
+
<Sonner
|
|
70
|
+
position={position}
|
|
71
|
+
expand={expand}
|
|
72
|
+
richColors={richColors}
|
|
73
|
+
closeButton={closeButton}
|
|
74
|
+
theme={theme}
|
|
75
|
+
toastOptions={{
|
|
76
|
+
style: {
|
|
77
|
+
background: "hsl(var(--card))",
|
|
78
|
+
border: "1px solid hsl(var(--border))",
|
|
79
|
+
color: "hsl(var(--foreground))",
|
|
80
|
+
},
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tooltip Component
|
|
3
|
+
*
|
|
4
|
+
* A compact, themeable tooltip built on Radix UI.
|
|
5
|
+
* Uses a portal to render at document body level, ensuring it's never clipped.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Portal-based rendering (never clipped by containers)
|
|
9
|
+
* - Configurable delay duration
|
|
10
|
+
* - Automatic position flipping when near edges
|
|
11
|
+
* - Theme-aware styling
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
16
|
+
|
|
17
|
+
import { cn } from "../utils/cn";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* TooltipProvider - Wrap your app with this for shared tooltip delay behavior
|
|
21
|
+
*/
|
|
22
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* TooltipRoot - The root component for controlled usage
|
|
26
|
+
*/
|
|
27
|
+
const TooltipRoot = TooltipPrimitive.Root;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* TooltipTrigger - The element that triggers the tooltip
|
|
31
|
+
*/
|
|
32
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* TooltipPortal - Portals the content to document.body
|
|
36
|
+
*/
|
|
37
|
+
const TooltipPortal = TooltipPrimitive.Portal;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* TooltipContent - The styled tooltip content
|
|
41
|
+
*/
|
|
42
|
+
const TooltipContent = React.forwardRef<
|
|
43
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
44
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
45
|
+
>(({ className, sideOffset = 6, ...props }, ref) => (
|
|
46
|
+
<TooltipPrimitive.Portal>
|
|
47
|
+
<TooltipPrimitive.Content
|
|
48
|
+
ref={ref}
|
|
49
|
+
sideOffset={sideOffset}
|
|
50
|
+
className={cn(
|
|
51
|
+
// Base styles
|
|
52
|
+
"z-[99999] overflow-hidden",
|
|
53
|
+
"px-2.5 py-1.5 rounded-md",
|
|
54
|
+
// Colors
|
|
55
|
+
"bg-popover text-popover-foreground",
|
|
56
|
+
"border border-border",
|
|
57
|
+
// Typography
|
|
58
|
+
"text-xs font-medium",
|
|
59
|
+
// Shadow
|
|
60
|
+
"shadow-md",
|
|
61
|
+
// Animation
|
|
62
|
+
"animate-in fade-in-0 zoom-in-95",
|
|
63
|
+
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
64
|
+
"data-[side=bottom]:slide-in-from-top-2",
|
|
65
|
+
"data-[side=left]:slide-in-from-right-2",
|
|
66
|
+
"data-[side=right]:slide-in-from-left-2",
|
|
67
|
+
"data-[side=top]:slide-in-from-bottom-2",
|
|
68
|
+
className
|
|
69
|
+
)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
</TooltipPrimitive.Portal>
|
|
73
|
+
));
|
|
74
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* TooltipArrow - Optional arrow pointing to the trigger
|
|
78
|
+
*/
|
|
79
|
+
const TooltipArrow = React.forwardRef<
|
|
80
|
+
React.ElementRef<typeof TooltipPrimitive.Arrow>,
|
|
81
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Arrow>
|
|
82
|
+
>(({ className, ...props }, ref) => (
|
|
83
|
+
<TooltipPrimitive.Arrow
|
|
84
|
+
ref={ref}
|
|
85
|
+
className={cn("fill-popover", className)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
));
|
|
89
|
+
TooltipArrow.displayName = TooltipPrimitive.Arrow.displayName;
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Simple Tooltip API (backwards compatible with existing usage)
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
export interface TooltipProps {
|
|
96
|
+
/**
|
|
97
|
+
* Tooltip content - can be string or ReactNode for rich content
|
|
98
|
+
*/
|
|
99
|
+
content: React.ReactNode;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Element to wrap with tooltip
|
|
103
|
+
*/
|
|
104
|
+
children: React.ReactElement;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Position of the tooltip relative to the trigger
|
|
108
|
+
* @default "top"
|
|
109
|
+
*/
|
|
110
|
+
position?: "top" | "bottom" | "left" | "right";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Delay before showing tooltip (in milliseconds)
|
|
114
|
+
* @default 200
|
|
115
|
+
*/
|
|
116
|
+
delayDuration?: number;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Offset from the trigger element (in pixels)
|
|
120
|
+
* @default 6
|
|
121
|
+
*/
|
|
122
|
+
sideOffset?: number;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Maximum width of the tooltip
|
|
126
|
+
* @default 250
|
|
127
|
+
*/
|
|
128
|
+
maxWidth?: number;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Whether the tooltip is disabled
|
|
132
|
+
* @default false
|
|
133
|
+
*/
|
|
134
|
+
disabled?: boolean;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Additional class names for the tooltip content
|
|
138
|
+
*/
|
|
139
|
+
className?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Tooltip component
|
|
144
|
+
*
|
|
145
|
+
* Simple wrapper around Radix Tooltip for ease of use.
|
|
146
|
+
* Renders in a portal so it's never clipped by containers.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* <Tooltip content="Hello world">
|
|
150
|
+
* <button>Hover me</button>
|
|
151
|
+
* </Tooltip>
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* <Tooltip content="Click to save" position="bottom" delayDuration={0}>
|
|
155
|
+
* <button>Save</button>
|
|
156
|
+
* </Tooltip>
|
|
157
|
+
*/
|
|
158
|
+
function Tooltip({
|
|
159
|
+
content,
|
|
160
|
+
children,
|
|
161
|
+
position = "top",
|
|
162
|
+
delayDuration = 200,
|
|
163
|
+
sideOffset = 6,
|
|
164
|
+
maxWidth = 250,
|
|
165
|
+
disabled = false,
|
|
166
|
+
className,
|
|
167
|
+
}: TooltipProps) {
|
|
168
|
+
// Don't render tooltip if no content or disabled
|
|
169
|
+
if (!content || disabled) {
|
|
170
|
+
return children;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<TooltipPrimitive.Provider delayDuration={delayDuration}>
|
|
175
|
+
<TooltipPrimitive.Root>
|
|
176
|
+
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
|
|
177
|
+
<TooltipContent
|
|
178
|
+
side={position}
|
|
179
|
+
sideOffset={sideOffset}
|
|
180
|
+
className={className}
|
|
181
|
+
style={{ maxWidth: `${maxWidth}px` }}
|
|
182
|
+
>
|
|
183
|
+
{content}
|
|
184
|
+
</TooltipContent>
|
|
185
|
+
</TooltipPrimitive.Root>
|
|
186
|
+
</TooltipPrimitive.Provider>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Export primitives for advanced usage
|
|
191
|
+
export {
|
|
192
|
+
Tooltip,
|
|
193
|
+
TooltipProvider,
|
|
194
|
+
TooltipRoot,
|
|
195
|
+
TooltipTrigger,
|
|
196
|
+
TooltipPortal,
|
|
197
|
+
TooltipContent,
|
|
198
|
+
TooltipArrow,
|
|
199
|
+
};
|