@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.
- package/README.md +42 -0
- package/package.json +37 -0
- package/registry/accordion.json +18 -0
- package/registry/alert.json +18 -0
- package/registry/avatar.json +17 -0
- package/registry/badge.json +18 -0
- package/registry/banner.json +19 -0
- package/registry/breadcrumb.json +17 -0
- package/registry/button.json +18 -0
- package/registry/card.json +17 -0
- package/registry/checkbox.json +17 -0
- package/registry/command.json +25 -0
- package/registry/dialog.json +18 -0
- package/registry/dropdown-menu.json +18 -0
- package/registry/index.json +333 -0
- package/registry/input-group.json +21 -0
- package/registry/input.json +17 -0
- package/registry/kbd.json +18 -0
- package/registry/label.json +18 -0
- package/registry/list.json +20 -0
- package/registry/popover.json +19 -0
- package/registry/radio-group.json +18 -0
- package/registry/select.json +18 -0
- package/registry/separator.json +17 -0
- package/registry/sheet.json +19 -0
- package/registry/skeleton.json +17 -0
- package/registry/switch.json +17 -0
- package/registry/tabs.json +17 -0
- package/registry/textarea.json +17 -0
- package/registry/theme-manager.json +38 -0
- package/registry/toast.json +20 -0
- package/registry/tooltip.json +18 -0
- package/src/index.css +11 -0
- package/src/lib/cn.ts +9 -0
- package/src/registry/components/ui/accordion.tsx +132 -0
- package/src/registry/components/ui/alert.tsx +155 -0
- package/src/registry/components/ui/avatar.tsx +114 -0
- package/src/registry/components/ui/badge.tsx +50 -0
- package/src/registry/components/ui/banner.tsx +189 -0
- package/src/registry/components/ui/breadcrumb.tsx +136 -0
- package/src/registry/components/ui/button.tsx +63 -0
- package/src/registry/components/ui/card.tsx +113 -0
- package/src/registry/components/ui/checkbox.tsx +74 -0
- package/src/registry/components/ui/command.tsx +286 -0
- package/src/registry/components/ui/dialog.tsx +195 -0
- package/src/registry/components/ui/dropdown-menu.tsx +265 -0
- package/src/registry/components/ui/input-group.tsx +80 -0
- package/src/registry/components/ui/input.tsx +28 -0
- package/src/registry/components/ui/kbd.tsx +73 -0
- package/src/registry/components/ui/label.tsx +30 -0
- package/src/registry/components/ui/list.tsx +222 -0
- package/src/registry/components/ui/popover.tsx +90 -0
- package/src/registry/components/ui/radio-group.tsx +95 -0
- package/src/registry/components/ui/select.tsx +153 -0
- package/src/registry/components/ui/separator.tsx +37 -0
- package/src/registry/components/ui/sheet.tsx +216 -0
- package/src/registry/components/ui/skeleton.tsx +24 -0
- package/src/registry/components/ui/switch.tsx +68 -0
- package/src/registry/components/ui/tabs.tsx +204 -0
- package/src/registry/components/ui/textarea.tsx +82 -0
- package/src/registry/components/ui/theme-toggle.tsx +200 -0
- package/src/registry/components/ui/toast.tsx +124 -0
- package/src/registry/components/ui/tooltip.tsx +41 -0
- package/src/registry/index.ts +50 -0
- package/src/registry/metadata.ts +167 -0
- package/src/registry/providers/theme-provider.tsx +208 -0
- package/src/registry/providers/theme-script.tsx +58 -0
- 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
|
+
};
|