@nikala-ui/core 0.0.0-nightly.bb5956a

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.
Files changed (68) hide show
  1. package/README.md +42 -0
  2. package/package.json +37 -0
  3. package/registry/accordion.json +18 -0
  4. package/registry/alert.json +18 -0
  5. package/registry/avatar.json +17 -0
  6. package/registry/badge.json +18 -0
  7. package/registry/banner.json +19 -0
  8. package/registry/breadcrumb.json +17 -0
  9. package/registry/button.json +18 -0
  10. package/registry/card.json +17 -0
  11. package/registry/checkbox.json +17 -0
  12. package/registry/command.json +25 -0
  13. package/registry/dialog.json +18 -0
  14. package/registry/dropdown-menu.json +18 -0
  15. package/registry/index.json +333 -0
  16. package/registry/input-group.json +21 -0
  17. package/registry/input.json +17 -0
  18. package/registry/kbd.json +18 -0
  19. package/registry/label.json +18 -0
  20. package/registry/list.json +20 -0
  21. package/registry/popover.json +19 -0
  22. package/registry/radio-group.json +18 -0
  23. package/registry/select.json +18 -0
  24. package/registry/separator.json +17 -0
  25. package/registry/sheet.json +19 -0
  26. package/registry/skeleton.json +17 -0
  27. package/registry/switch.json +17 -0
  28. package/registry/tabs.json +17 -0
  29. package/registry/textarea.json +17 -0
  30. package/registry/theme-manager.json +38 -0
  31. package/registry/toast.json +20 -0
  32. package/registry/tooltip.json +18 -0
  33. package/src/index.css +11 -0
  34. package/src/lib/cn.ts +9 -0
  35. package/src/registry/components/ui/accordion.tsx +132 -0
  36. package/src/registry/components/ui/alert.tsx +155 -0
  37. package/src/registry/components/ui/avatar.tsx +114 -0
  38. package/src/registry/components/ui/badge.tsx +50 -0
  39. package/src/registry/components/ui/banner.tsx +189 -0
  40. package/src/registry/components/ui/breadcrumb.tsx +136 -0
  41. package/src/registry/components/ui/button.tsx +63 -0
  42. package/src/registry/components/ui/card.tsx +113 -0
  43. package/src/registry/components/ui/checkbox.tsx +74 -0
  44. package/src/registry/components/ui/command.tsx +286 -0
  45. package/src/registry/components/ui/dialog.tsx +195 -0
  46. package/src/registry/components/ui/dropdown-menu.tsx +265 -0
  47. package/src/registry/components/ui/input-group.tsx +80 -0
  48. package/src/registry/components/ui/input.tsx +28 -0
  49. package/src/registry/components/ui/kbd.tsx +73 -0
  50. package/src/registry/components/ui/label.tsx +30 -0
  51. package/src/registry/components/ui/list.tsx +222 -0
  52. package/src/registry/components/ui/popover.tsx +90 -0
  53. package/src/registry/components/ui/radio-group.tsx +95 -0
  54. package/src/registry/components/ui/select.tsx +153 -0
  55. package/src/registry/components/ui/separator.tsx +37 -0
  56. package/src/registry/components/ui/sheet.tsx +216 -0
  57. package/src/registry/components/ui/skeleton.tsx +24 -0
  58. package/src/registry/components/ui/switch.tsx +68 -0
  59. package/src/registry/components/ui/tabs.tsx +204 -0
  60. package/src/registry/components/ui/textarea.tsx +82 -0
  61. package/src/registry/components/ui/theme-toggle.tsx +200 -0
  62. package/src/registry/components/ui/toast.tsx +124 -0
  63. package/src/registry/components/ui/tooltip.tsx +41 -0
  64. package/src/registry/index.ts +50 -0
  65. package/src/registry/metadata.ts +167 -0
  66. package/src/registry/providers/theme-provider.tsx +208 -0
  67. package/src/registry/providers/theme-script.tsx +58 -0
  68. package/src/registry/providers/theme-transitions.ts +108 -0
@@ -0,0 +1,204 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ splitProps,
5
+ Show,
6
+ type Component,
7
+ type JSX,
8
+ type Accessor,
9
+ } from "solid-js";
10
+ import { createControllableSignal } from "@nikala-ui/hooks";
11
+ import { cn } from "@/lib/cn";
12
+
13
+ interface TabsContextValue {
14
+ value: Accessor<string | undefined>;
15
+ setValue: (value: string) => void;
16
+ orientation: Accessor<"horizontal" | "vertical">;
17
+ }
18
+
19
+ const TabsContext = createContext<TabsContextValue>();
20
+
21
+ export interface TabsProps
22
+ extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "onChange"> {
23
+ /** Controlled active tab value */
24
+ value?: string;
25
+ /** Uncontrolled default active tab value */
26
+ defaultValue?: string;
27
+ /** Callback fired when active tab changes */
28
+ onChange?: (value: string) => void;
29
+ /** Layout orientation of tab triggers and content */
30
+ orientation?: "horizontal" | "vertical";
31
+ class?: string;
32
+ }
33
+
34
+ /**
35
+ * Root Tabs container component managing active tab context state and orientation.
36
+ */
37
+ export const Tabs: Component<TabsProps> = (props) => {
38
+ const [local, rest] = splitProps(props, [
39
+ "value",
40
+ "defaultValue",
41
+ "onChange",
42
+ "orientation",
43
+ "class",
44
+ "children",
45
+ ]);
46
+
47
+ const [currentValue, setCurrentValue] = createControllableSignal<string>({
48
+ value: () => local.value,
49
+ defaultValue: local.defaultValue,
50
+ onChange: (val) => local.onChange?.(val),
51
+ });
52
+
53
+ const orientation = () => local.orientation || "horizontal";
54
+
55
+ const contextValue: TabsContextValue = {
56
+ value: currentValue,
57
+ setValue: (val: string) => setCurrentValue(val),
58
+ orientation,
59
+ };
60
+
61
+ return (
62
+ <TabsContext.Provider value={contextValue}>
63
+ <div
64
+ data-orientation={orientation()}
65
+ class={cn(
66
+ "w-full",
67
+ orientation() === "vertical" ? "flex flex-row gap-4" : "flex flex-col gap-2",
68
+ local.class
69
+ )}
70
+ {...rest}
71
+ >
72
+ {local.children}
73
+ </div>
74
+ </TabsContext.Provider>
75
+ );
76
+ };
77
+
78
+ export interface TabsListProps extends JSX.HTMLAttributes<HTMLDivElement> {
79
+ class?: string;
80
+ }
81
+
82
+ /**
83
+ * Container wrapper for Tab triggers.
84
+ */
85
+ export const TabsList: Component<TabsListProps> = (props) => {
86
+ const [local, rest] = splitProps(props, ["class", "children"]);
87
+ const context = useContext(TabsContext);
88
+
89
+ const isVertical = () => context?.orientation() === "vertical";
90
+
91
+ return (
92
+ <div
93
+ role="tablist"
94
+ aria-orientation={context?.orientation() || "horizontal"}
95
+ class={cn(
96
+ "inline-flex rounded-lg bg-muted p-1 text-muted-foreground",
97
+ isVertical()
98
+ ? "flex-col h-auto w-auto items-stretch justify-start"
99
+ : "h-9 items-center justify-center",
100
+ local.class
101
+ )}
102
+ {...rest}
103
+ >
104
+ {local.children}
105
+ </div>
106
+ );
107
+ };
108
+
109
+ export interface TabsTriggerProps
110
+ extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> {
111
+ /** Unique value identifier for this tab */
112
+ value: string;
113
+ class?: string;
114
+ }
115
+
116
+ /**
117
+ * Tab button trigger to activate a specific tab panel.
118
+ */
119
+ export const TabsTrigger: Component<TabsTriggerProps> = (props) => {
120
+ const [local, rest] = splitProps(props, [
121
+ "value",
122
+ "disabled",
123
+ "class",
124
+ "children",
125
+ "onClick",
126
+ ]);
127
+ const context = useContext(TabsContext);
128
+
129
+ if (!context) {
130
+ throw new Error("TabsTrigger must be used within a Tabs component");
131
+ }
132
+
133
+ const isSelected = () => context.value() === local.value;
134
+ const isVertical = () => context.orientation() === "vertical";
135
+
136
+ const handleClick = (
137
+ e: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }
138
+ ) => {
139
+ if (local.disabled) return;
140
+ context.setValue(local.value);
141
+ if (typeof local.onClick === "function") {
142
+ local.onClick(e);
143
+ }
144
+ };
145
+
146
+ return (
147
+ <button
148
+ type="button"
149
+ role="tab"
150
+ aria-selected={isSelected()}
151
+ data-state={isSelected() ? "active" : "inactive"}
152
+ data-orientation={context.orientation()}
153
+ disabled={local.disabled}
154
+ onClick={handleClick}
155
+ class={cn(
156
+ "inline-flex items-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm cursor-pointer",
157
+ isVertical() ? "justify-start py-1.5" : "justify-center",
158
+ local.class
159
+ )}
160
+ {...rest}
161
+ >
162
+ {local.children}
163
+ </button>
164
+ );
165
+ };
166
+
167
+ export interface TabsContentProps extends JSX.HTMLAttributes<HTMLDivElement> {
168
+ /** Value matching the corresponding tab trigger */
169
+ value: string;
170
+ class?: string;
171
+ }
172
+
173
+ /**
174
+ * Content panel revealed when the associated tab is active.
175
+ */
176
+ export const TabsContent: Component<TabsContentProps> = (props) => {
177
+ const [local, rest] = splitProps(props, ["value", "class", "children"]);
178
+ const context = useContext(TabsContext);
179
+
180
+ if (!context) {
181
+ throw new Error("TabsContent must be used within a Tabs component");
182
+ }
183
+
184
+ const isSelected = () => context.value() === local.value;
185
+ const isVertical = () => context.orientation() === "vertical";
186
+
187
+ return (
188
+ <Show when={isSelected()}>
189
+ <div
190
+ role="tabpanel"
191
+ data-state={isSelected() ? "active" : "inactive"}
192
+ data-orientation={context.orientation()}
193
+ class={cn(
194
+ "ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
195
+ isVertical() ? "flex-1 mt-0" : "mt-2",
196
+ local.class
197
+ )}
198
+ {...rest}
199
+ >
200
+ {local.children}
201
+ </div>
202
+ </Show>
203
+ );
204
+ };
@@ -0,0 +1,82 @@
1
+ // src/components/ui/textarea.tsx
2
+ import {
3
+ createSignal,
4
+ splitProps,
5
+ Show,
6
+ type Component,
7
+ type JSX,
8
+ } from "solid-js";
9
+ import { cn } from "@/lib/cn";
10
+
11
+ export interface TextareaProps
12
+ extends JSX.TextareaHTMLAttributes<HTMLTextAreaElement> {
13
+ /** Uncontrolled initial default value */
14
+ defaultValue?: string | number;
15
+ /** Maximum allowed character limit */
16
+ maxLength?: number;
17
+ /** Whether to show live character count badge */
18
+ showCount?: boolean;
19
+ class?: string;
20
+ }
21
+
22
+ /**
23
+ * Nikala UI Textarea component with optional live character counter and limit indicators.
24
+ */
25
+ export const Textarea: Component<TextareaProps> = (props) => {
26
+ const [local, rest] = splitProps(props, [
27
+ "maxLength",
28
+ "showCount",
29
+ "value",
30
+ "defaultValue",
31
+ "onInput",
32
+ "class",
33
+ ]);
34
+
35
+ /* Internal reactive signal for character count tracking */
36
+ const [currentValue, setCurrentValue] = createSignal<string>(
37
+ String(local.value || local.defaultValue || "")
38
+ );
39
+
40
+ const handleInput: JSX.EventHandlerUnion<HTMLTextAreaElement, InputEvent> = (
41
+ e
42
+ ) => {
43
+ setCurrentValue(e.currentTarget.value);
44
+ if (typeof local.onInput === "function") {
45
+ (local.onInput as (e: InputEvent) => void)(e);
46
+ }
47
+ };
48
+
49
+ const count = () => currentValue().length;
50
+ const isAtLimit = () =>
51
+ local.maxLength !== undefined && count() >= local.maxLength;
52
+
53
+ return (
54
+ <div class="relative flex flex-col w-full">
55
+ <textarea
56
+ value={local.value !== undefined ? String(local.value) : currentValue()}
57
+ maxLength={local.maxLength}
58
+ onInput={handleInput}
59
+ class={cn(
60
+ "flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-2xs transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
61
+ local.class
62
+ )}
63
+ {...rest}
64
+ />
65
+
66
+ {/* Dynamic Character Counter */}
67
+ <Show when={local.showCount ?? Boolean(local.maxLength)}>
68
+ <div
69
+ class={cn(
70
+ "mt-1 text-right text-[11px] font-mono transition-colors select-none",
71
+ isAtLimit() ? "text-rose-500 font-bold" : "text-muted-foreground"
72
+ )}
73
+ >
74
+ <span>{count()}</span>
75
+ <Show when={local.maxLength}>
76
+ <span> / {local.maxLength}</span>
77
+ </Show>
78
+ </div>
79
+ </Show>
80
+ </div>
81
+ );
82
+ };
@@ -0,0 +1,200 @@
1
+ import { splitProps, type Component, For, Show } from "solid-js";
2
+ import { Sun, Moon, Monitor } from "lucide-solid";
3
+ import {
4
+ useTheme,
5
+ type AccentColor,
6
+ type Radius,
7
+ type Theme,
8
+ } from "../../providers/theme-provider";
9
+ import {
10
+ runThemeTransition,
11
+ type ThemeEffect,
12
+ } from "../../providers/theme-transitions";
13
+ import { Button } from "./button";
14
+ import {
15
+ DropdownMenu,
16
+ DropdownMenuTrigger,
17
+ DropdownMenuContent,
18
+ DropdownMenuItem,
19
+ DropdownMenuLabel,
20
+ DropdownMenuSeparator,
21
+ } from "./dropdown-menu";
22
+ import { cn } from "@/lib/cn";
23
+
24
+ import { createColorMode } from "@nikala-ui/hooks";
25
+
26
+ export interface ThemeToggleProps {
27
+ /** Display mode: "mini" for compact dropdown, "max" for full customizer panel (default: "mini") */
28
+ mode?: "mini" | "max";
29
+ /** Transition animation effect when changing themes ("none", "circular", "fade") */
30
+ effect?: ThemeEffect;
31
+ class?: string;
32
+ }
33
+
34
+ const ACCENT_OPTIONS: { name: AccentColor; label: string; color: string }[] = [
35
+ { name: "wine", label: "Wine", color: "bg-[#722f37]" },
36
+ { name: "violet", label: "Violet", color: "bg-[#7c3aed]" },
37
+ { name: "sky", label: "Sky", color: "bg-[#0284c7]" },
38
+ { name: "emerald", label: "Emerald", color: "bg-[#059669]" },
39
+ { name: "rose", label: "Rose", color: "bg-[#e11d48]" },
40
+ { name: "amber", label: "Amber", color: "bg-[#d97706]" },
41
+ { name: "zinc", label: "Zinc", color: "bg-[#18181b]" },
42
+ ];
43
+
44
+ const RADIUS_OPTIONS: { value: Radius; label: string }[] = [
45
+ { value: "0", label: "0" },
46
+ { value: "0.3", label: "0.3" },
47
+ { value: "0.5", label: "0.5" },
48
+ { value: "0.75", label: "0.75" },
49
+ { value: "1.0", label: "1.0" },
50
+ ];
51
+
52
+ /**
53
+ * Interactive UI theme switcher supporting mini/max modes and View Transition animations.
54
+ */
55
+ export const ThemeToggle: Component<ThemeToggleProps> = (props) => {
56
+ const [local] = splitProps(props, ["mode", "effect", "class"]);
57
+ const { theme, setTheme, accent, setAccent, radius, setRadius } = useTheme();
58
+
59
+ const colorMode = createColorMode({
60
+ initialValue: theme(),
61
+ storageKey: "nikala-theme-mode",
62
+ });
63
+
64
+ const mode = () => local.mode || "mini";
65
+ const effect = () => local.effect || "none";
66
+
67
+ /* Reactive accessor determining whether dark mode is active via createColorMode hook */
68
+ const isDarkMode = () => {
69
+ const currentTheme = theme();
70
+ if (currentTheme === "dark") return true;
71
+ if (currentTheme === "light") return false;
72
+ return colorMode.isDark();
73
+ };
74
+
75
+ const changeThemeWithEffect = (newTheme: Theme, e: MouseEvent) => {
76
+ runThemeTransition(effect(), e, () => {
77
+ setTheme(newTheme);
78
+ colorMode.setMode(newTheme);
79
+ });
80
+ };
81
+
82
+ return (
83
+ <DropdownMenu placement="bottom-end">
84
+ <DropdownMenuTrigger
85
+ as={Button}
86
+ variant="outline"
87
+ size="icon"
88
+ class={cn("relative h-9 w-9 cursor-pointer", local.class)}
89
+ >
90
+ {/* Reactive Sun / Moon Icon Toggle */}
91
+ <Show
92
+ when={isDarkMode()}
93
+ fallback={<Sun class="h-4 w-4 text-foreground transition-transform" />}
94
+ >
95
+ <Moon class="h-4 w-4 text-foreground transition-transform" />
96
+ </Show>
97
+
98
+ <span class="sr-only">Toggle theme</span>
99
+ </DropdownMenuTrigger>
100
+
101
+ <Show
102
+ when={mode() === "max"}
103
+ fallback={
104
+ /* Mini Mode: Compact Dropdown */
105
+ <DropdownMenuContent>
106
+ <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect("light", e)}>
107
+ <Sun class="mr-2 h-4 w-4 text-muted-foreground" />
108
+ Light
109
+ </DropdownMenuItem>
110
+
111
+ <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect("dark", e)}>
112
+ <Moon class="mr-2 h-4 w-4 text-muted-foreground" />
113
+ Dark
114
+ </DropdownMenuItem>
115
+
116
+ <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect("system", e)}>
117
+ <Monitor class="mr-2 h-4 w-4 text-muted-foreground" />
118
+ System
119
+ </DropdownMenuItem>
120
+ </DropdownMenuContent>
121
+ }
122
+ >
123
+ {/* Max Mode: Full Theme Customizer Panel */}
124
+ <DropdownMenuContent class="w-64 p-3">
125
+ <DropdownMenuLabel class="px-0 pt-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
126
+ Theme Mode
127
+ </DropdownMenuLabel>
128
+ <div class="grid grid-cols-3 gap-1 my-1.5">
129
+ <Button
130
+ variant={theme() === "light" ? "default" : "outline"}
131
+ size="sm"
132
+ onClick={(e: MouseEvent) => changeThemeWithEffect("light", e)}
133
+ class="h-8 text-xs cursor-pointer"
134
+ >
135
+ Light
136
+ </Button>
137
+ <Button
138
+ variant={theme() === "dark" ? "default" : "outline"}
139
+ size="sm"
140
+ onClick={(e: MouseEvent) => changeThemeWithEffect("dark", e)}
141
+ class="h-8 text-xs cursor-pointer"
142
+ >
143
+ Dark
144
+ </Button>
145
+ <Button
146
+ variant={theme() === "system" ? "default" : "outline"}
147
+ size="sm"
148
+ onClick={(e: MouseEvent) => changeThemeWithEffect("system", e)}
149
+ class="h-8 text-xs cursor-pointer"
150
+ >
151
+ System
152
+ </Button>
153
+ </div>
154
+
155
+ <DropdownMenuSeparator class="my-2" />
156
+
157
+ <DropdownMenuLabel class="px-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
158
+ Brand Accent Color
159
+ </DropdownMenuLabel>
160
+ <div class="flex flex-wrap gap-1.5 my-1.5">
161
+ <For each={ACCENT_OPTIONS}>
162
+ {(opt) => (
163
+ <button
164
+ type="button"
165
+ title={opt.label}
166
+ onClick={() => setAccent(opt.name)}
167
+ class={cn(
168
+ "h-6 w-6 rounded-md transition-all cursor-pointer border border-border flex items-center justify-center",
169
+ opt.color,
170
+ accent() === opt.name ? "ring-2 ring-primary ring-offset-2 ring-offset-background scale-110" : "hover:scale-105"
171
+ )}
172
+ />
173
+ )}
174
+ </For>
175
+ </div>
176
+
177
+ <DropdownMenuSeparator class="my-2" />
178
+
179
+ <DropdownMenuLabel class="px-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
180
+ Border Radius
181
+ </DropdownMenuLabel>
182
+ <div class="grid grid-cols-5 gap-1 my-1.5">
183
+ <For each={RADIUS_OPTIONS}>
184
+ {(r) => (
185
+ <Button
186
+ variant={radius() === r.value ? "default" : "outline"}
187
+ size="sm"
188
+ onClick={() => setRadius(r.value)}
189
+ class="h-7 text-xs px-1 cursor-pointer"
190
+ >
191
+ {r.label}
192
+ </Button>
193
+ )}
194
+ </For>
195
+ </div>
196
+ </DropdownMenuContent>
197
+ </Show>
198
+ </DropdownMenu>
199
+ );
200
+ };
@@ -0,0 +1,124 @@
1
+ import { splitProps, type Component, type JSX, type ComponentProps } from "solid-js";
2
+ import { Toast as KobalteToast, toaster } from "@kobalte/core/toast";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { X, CircleCheck, Info, CircleAlert, TriangleAlert } from "lucide-solid";
5
+ import { cn } from "@/lib/cn";
6
+
7
+ export const toastVariants = cva(
8
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-lg border p-4 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--kb-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--kb-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[opened]:animate-in data-[closed]:animate-out data-[swipe=end]:animate-out data-[closed]:fade-out-80 data-[closed]:slide-out-to-right-full data-[opened]:slide-in-from-top-full data-[opened]:sm:slide-in-from-bottom-full",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "border-border bg-background text-foreground",
13
+ success: "border-emerald-500/20 bg-emerald-500/10 text-emerald-900 dark:text-emerald-200 border-emerald-500/30",
14
+ destructive: "border-destructive/30 bg-destructive/10 text-destructive dark:text-red-300",
15
+ warning: "border-amber-500/20 bg-amber-500/10 text-amber-900 dark:text-amber-200 border-amber-500/30",
16
+ info: "border-sky-500/20 bg-sky-500/10 text-sky-900 dark:text-sky-200 border-sky-500/30",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "default",
21
+ },
22
+ }
23
+ );
24
+
25
+ export interface ToastProps
26
+ extends ComponentProps<typeof KobalteToast>,
27
+ VariantProps<typeof toastVariants> {
28
+ class?: string;
29
+ }
30
+
31
+ export const Toast: Component<ToastProps> = (props) => {
32
+ const [local, rest] = splitProps(props, ["class", "variant"]);
33
+
34
+ return (
35
+ <KobalteToast
36
+ class={cn(toastVariants({ variant: local.variant }), local.class)}
37
+ {...rest}
38
+ />
39
+ );
40
+ };
41
+
42
+ export const ToastTitle: Component<ComponentProps<typeof KobalteToast.Title>> = (props) => {
43
+ const [local, rest] = splitProps(props, ["class"]);
44
+
45
+ return (
46
+ <KobalteToast.Title
47
+ class={cn("text-sm font-semibold [&+div]:text-xs", local.class)}
48
+ {...rest}
49
+ />
50
+ );
51
+ };
52
+
53
+ export const ToastDescription: Component<ComponentProps<typeof KobalteToast.Description>> = (props) => {
54
+ const [local, rest] = splitProps(props, ["class"]);
55
+
56
+ return (
57
+ <KobalteToast.Description
58
+ class={cn("text-sm opacity-90", local.class)}
59
+ {...rest}
60
+ />
61
+ );
62
+ };
63
+
64
+ export const ToastCloseButton: Component<ComponentProps<typeof KobalteToast.CloseButton>> = (props) => {
65
+ const [local, rest] = splitProps(props, ["class", "children"]);
66
+
67
+ return (
68
+ <KobalteToast.CloseButton
69
+ class={cn(
70
+ "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100",
71
+ local.class
72
+ )}
73
+ {...rest}
74
+ >
75
+ {local.children || <X class="h-4 w-4" />}
76
+ </KobalteToast.CloseButton>
77
+ );
78
+ };
79
+
80
+ export const ToastRegion: Component<ComponentProps<typeof KobalteToast.Region>> = (props) => {
81
+ const [local, rest] = splitProps(props, ["class"]);
82
+
83
+ return (
84
+ <KobalteToast.Region
85
+ class={cn(
86
+ "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
87
+ local.class
88
+ )}
89
+ {...rest}
90
+ />
91
+ );
92
+ };
93
+
94
+ export const ToastList: Component<ComponentProps<typeof KobalteToast.List>> = (props) => {
95
+ const [local, rest] = splitProps(props, ["class"]);
96
+
97
+ return <KobalteToast.List class={cn("flex flex-col gap-2", local.class)} {...rest} />;
98
+ };
99
+
100
+ /* --- Helper Function to Trigger Toast Notifications --- */
101
+ export interface ShowToastOptions {
102
+ title: string;
103
+ description?: string;
104
+ variant?: "default" | "success" | "destructive" | "warning" | "info";
105
+ duration?: number;
106
+ }
107
+
108
+ export const showToast = (options: ShowToastOptions) => {
109
+ return toaster.show((props) => (
110
+ <Toast toastId={props.toastId} variant={options.variant || "default"}>
111
+ <div class="flex items-start gap-3">
112
+ {options.variant === "success" && <CircleCheck class="h-5 w-5 text-emerald-500 shrink-0 mt-0.5" />}
113
+ {options.variant === "destructive" && <CircleAlert class="h-5 w-5 text-red-500 shrink-0 mt-0.5" />}
114
+ {options.variant === "warning" && <TriangleAlert class="h-5 w-5 text-amber-500 shrink-0 mt-0.5" />}
115
+ {options.variant === "info" && <Info class="h-5 w-5 text-sky-500 shrink-0 mt-0.5" />}
116
+ <div class="grid gap-1">
117
+ <ToastTitle>{options.title}</ToastTitle>
118
+ {options.description && <ToastDescription>{options.description}</ToastDescription>}
119
+ </div>
120
+ </div>
121
+ <ToastCloseButton />
122
+ </Toast>
123
+ ));
124
+ };
@@ -0,0 +1,41 @@
1
+ import { splitProps, type Component, type ComponentProps } from "solid-js";
2
+ import { Tooltip as KobalteTooltip } from "@kobalte/core/tooltip";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export const Tooltip = KobalteTooltip;
6
+
7
+ export const TooltipTrigger = KobalteTooltip.Trigger;
8
+
9
+ export const TooltipArrow: Component<ComponentProps<typeof KobalteTooltip.Arrow>> = (props) => {
10
+ const [local, rest] = splitProps(props, ["class"]);
11
+
12
+ return (
13
+ <KobalteTooltip.Arrow
14
+ size={8}
15
+ class={cn("fill-popover stroke-border", local.class)}
16
+ {...rest}
17
+ />
18
+ );
19
+ };
20
+
21
+ export interface TooltipContentProps extends ComponentProps<typeof KobalteTooltip.Content> {
22
+ class?: string;
23
+ }
24
+
25
+ export const TooltipContent: Component<TooltipContentProps> = (props) => {
26
+ const [local, rest] = splitProps(props, ["class", "children"]);
27
+
28
+ return (
29
+ <KobalteTooltip.Portal>
30
+ <KobalteTooltip.Content
31
+ class={cn(
32
+ "z-50 rounded-md border border-border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md transition-all animate-in fade-in-0 zoom-in-95 data-closed:animate-out data-[closed]:fade-out-0 data-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",
33
+ local.class
34
+ )}
35
+ {...rest}
36
+ >
37
+ {local.children}
38
+ </KobalteTooltip.Content>
39
+ </KobalteTooltip.Portal>
40
+ );
41
+ };