@yugnex/nexui-react 2.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.
Files changed (46) hide show
  1. package/dist/card.d.ts +31 -0
  2. package/dist/card.d.ts.map +1 -0
  3. package/dist/card.js +73 -0
  4. package/dist/card.js.map +1 -0
  5. package/dist/index.d.ts +10 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +15 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/modal.d.ts +13 -0
  10. package/dist/modal.d.ts.map +1 -0
  11. package/dist/modal.js +117 -0
  12. package/dist/modal.js.map +1 -0
  13. package/dist/primitives.d.ts +162 -0
  14. package/dist/primitives.d.ts.map +1 -0
  15. package/dist/primitives.js +100 -0
  16. package/dist/primitives.js.map +1 -0
  17. package/dist/provider.d.ts +16 -0
  18. package/dist/provider.d.ts.map +1 -0
  19. package/dist/provider.js +58 -0
  20. package/dist/provider.js.map +1 -0
  21. package/dist/select.d.ts +27 -0
  22. package/dist/select.d.ts.map +1 -0
  23. package/dist/select.js +172 -0
  24. package/dist/select.js.map +1 -0
  25. package/dist/tabs.d.ts +33 -0
  26. package/dist/tabs.d.ts.map +1 -0
  27. package/dist/tabs.js +74 -0
  28. package/dist/tabs.js.map +1 -0
  29. package/dist/toast.d.ts +24 -0
  30. package/dist/toast.d.ts.map +1 -0
  31. package/dist/toast.js +111 -0
  32. package/dist/toast.js.map +1 -0
  33. package/dist/tooltip.d.ts +12 -0
  34. package/dist/tooltip.d.ts.map +1 -0
  35. package/dist/tooltip.js +52 -0
  36. package/dist/tooltip.js.map +1 -0
  37. package/package.json +44 -0
  38. package/src/card.tsx +137 -0
  39. package/src/index.ts +35 -0
  40. package/src/modal.tsx +172 -0
  41. package/src/primitives.tsx +344 -0
  42. package/src/provider.tsx +88 -0
  43. package/src/select.tsx +296 -0
  44. package/src/tabs.tsx +150 -0
  45. package/src/toast.tsx +178 -0
  46. package/src/tooltip.tsx +90 -0
package/src/select.tsx ADDED
@@ -0,0 +1,296 @@
1
+ "use client";
2
+ // @yugnex/nexui-react — Select
3
+ // Fully custom, keyboard-navigable, ARIA-compliant dropdown select.
4
+ // No native <select>. Works with keyboard (arrows, Enter, Escape, Home/End).
5
+
6
+ import React, {
7
+ createContext,
8
+ useCallback,
9
+ useContext,
10
+ useEffect,
11
+ useId,
12
+ useRef,
13
+ useState,
14
+ type ReactNode,
15
+ type CSSProperties,
16
+ type KeyboardEvent,
17
+ } from "react";
18
+
19
+ interface SelectContextValue {
20
+ value: string;
21
+ open: boolean;
22
+ onChange: (v: string, label: string) => void;
23
+ close: () => void;
24
+ labelFor: (v: string) => string;
25
+ registerOption: (v: string, label: string) => void;
26
+ focusedIdx: number;
27
+ options: Array<{ value: string; label: string; disabled?: boolean }>;
28
+ setFocusedIdx: (i: number) => void;
29
+ }
30
+ const SelectContext = createContext<SelectContextValue>({} as SelectContextValue);
31
+
32
+ interface SelectProps {
33
+ value?: string;
34
+ defaultValue?: string;
35
+ onChange?: (value: string) => void;
36
+ placeholder?: string;
37
+ disabled?: boolean;
38
+ size?: "sm" | "md" | "lg";
39
+ error?: string;
40
+ label?: string;
41
+ children?: ReactNode;
42
+ className?: string;
43
+ style?: CSSProperties;
44
+ }
45
+
46
+ export function Select({
47
+ value: controlledValue,
48
+ defaultValue = "",
49
+ onChange,
50
+ placeholder = "Select...",
51
+ disabled,
52
+ size = "md",
53
+ error,
54
+ label,
55
+ children,
56
+ className,
57
+ style,
58
+ }: SelectProps) {
59
+ const [internal, setInternal] = useState(defaultValue);
60
+ const [open, setOpen] = useState(false);
61
+ const [focusedIdx, setFocusedIdx] = useState(-1);
62
+ const [options, setOptions] = useState<Array<{ value: string; label: string; disabled?: boolean }>>([]);
63
+ const [labelMap, setLabelMap] = useState<Record<string, string>>({});
64
+ const triggerRef = useRef<HTMLButtonElement>(null);
65
+ const listRef = useRef<HTMLUListElement>(null);
66
+ const id = useId();
67
+
68
+ const value = controlledValue !== undefined ? controlledValue : internal;
69
+
70
+ const registerOption = useCallback((v: string, lbl: string) => {
71
+ setLabelMap(m => ({ ...m, [v]: lbl }));
72
+ setOptions(prev => {
73
+ if (prev.find(o => o.value === v)) return prev;
74
+ return [...prev, { value: v, label: lbl }];
75
+ });
76
+ }, []);
77
+
78
+ const close = useCallback(() => {
79
+ setOpen(false);
80
+ setFocusedIdx(-1);
81
+ triggerRef.current?.focus();
82
+ }, []);
83
+
84
+ const handleChange = useCallback((v: string, lbl: string) => {
85
+ setInternal(v);
86
+ onChange?.(v);
87
+ close();
88
+ }, [onChange, close]);
89
+
90
+ useEffect(() => {
91
+ if (!open) return;
92
+ const handler = (e: MouseEvent) => {
93
+ const trigger = triggerRef.current;
94
+ const list = listRef.current;
95
+ if (!trigger?.contains(e.target as Node) && !list?.contains(e.target as Node)) {
96
+ close();
97
+ }
98
+ };
99
+ document.addEventListener("mousedown", handler);
100
+ return () => document.removeEventListener("mousedown", handler);
101
+ }, [open, close]);
102
+
103
+ const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
104
+ if (disabled) return;
105
+ const enabled = options.filter(o => !o.disabled);
106
+ switch (e.key) {
107
+ case "Enter":
108
+ case " ":
109
+ e.preventDefault();
110
+ if (!open) { setOpen(true); setFocusedIdx(0); }
111
+ else if (focusedIdx >= 0) handleChange(enabled[focusedIdx]?.value ?? "", enabled[focusedIdx]?.label ?? "");
112
+ break;
113
+ case "Escape":
114
+ e.preventDefault();
115
+ close();
116
+ break;
117
+ case "ArrowDown":
118
+ e.preventDefault();
119
+ if (!open) { setOpen(true); setFocusedIdx(0); }
120
+ else setFocusedIdx(i => Math.min(i + 1, enabled.length - 1));
121
+ break;
122
+ case "ArrowUp":
123
+ e.preventDefault();
124
+ setFocusedIdx(i => Math.max(i - 1, 0));
125
+ break;
126
+ case "Home":
127
+ e.preventDefault();
128
+ setFocusedIdx(0);
129
+ break;
130
+ case "End":
131
+ e.preventDefault();
132
+ setFocusedIdx(enabled.length - 1);
133
+ break;
134
+ }
135
+ };
136
+
137
+ const sizeH: Record<string, string> = { sm: "28px", md: "34px", lg: "40px" };
138
+ const sizeFS: Record<string, string> = { sm: "11px", md: "13px", lg: "14px" };
139
+ const h = sizeH[size] ?? sizeH.md;
140
+ const fs = sizeFS[size] ?? sizeFS.md;
141
+
142
+ const displayLabel = value ? (labelMap[value] ?? value) : "";
143
+
144
+ return (
145
+ <SelectContext.Provider value={{
146
+ value, open, onChange: handleChange, close,
147
+ labelFor: (v) => labelMap[v] ?? v,
148
+ registerOption, focusedIdx, options, setFocusedIdx,
149
+ }}>
150
+ <div className={className} style={{ position: "relative", display: "inline-block", width: "100%", ...style }}>
151
+ {label && (
152
+ <label
153
+ htmlFor={`${id}-btn`}
154
+ style={{ display: "block", fontSize: "12px", fontWeight: 500, color: "var(--nx-text-2, #8B949E)", marginBottom: 5, fontFamily: "var(--nx-font-sans, system-ui, sans-serif)" }}
155
+ >
156
+ {label}
157
+ </label>
158
+ )}
159
+ <button
160
+ id={`${id}-btn`}
161
+ ref={triggerRef}
162
+ type="button"
163
+ disabled={disabled}
164
+ aria-haspopup="listbox"
165
+ aria-expanded={open}
166
+ aria-controls={`${id}-list`}
167
+ onClick={() => !disabled && setOpen(o => !o)}
168
+ onKeyDown={handleKeyDown}
169
+ style={{
170
+ width: "100%",
171
+ height: h,
172
+ padding: "0 10px",
173
+ display: "flex",
174
+ alignItems: "center",
175
+ justifyContent: "space-between",
176
+ gap: 8,
177
+ background: "var(--nx-bg-elevated, #1C2128)",
178
+ border: `1px solid ${error ? "var(--nx-error, #EF4444)" : open ? "var(--nx-border-focus, rgba(232,144,16,0.60))" : "var(--nx-border, rgba(255,255,255,0.08))"}`,
179
+ borderRadius: "6px",
180
+ color: displayLabel ? "var(--nx-text, #E6EDF3)" : "var(--nx-text-4, #484F58)",
181
+ fontFamily: "var(--nx-font-sans, system-ui, sans-serif)",
182
+ fontSize: fs,
183
+ fontWeight: 400,
184
+ cursor: disabled ? "not-allowed" : "pointer",
185
+ opacity: disabled ? 0.45 : 1,
186
+ outline: "none",
187
+ boxShadow: open ? `0 0 0 3px rgba(232,144,16,0.12)` : "none",
188
+ transition: "border-color 150ms ease, box-shadow 150ms ease",
189
+ }}
190
+ >
191
+ <span style={{ flex: 1, textAlign: "left", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
192
+ {displayLabel || placeholder}
193
+ </span>
194
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" style={{ transform: open ? "rotate(180deg)" : "none", transition: "transform 150ms ease", flexShrink: 0, opacity: 0.6 }}>
195
+ <path d="M6 9l6 6 6-6"/>
196
+ </svg>
197
+ </button>
198
+ {error && <span style={{ fontSize: "11px", color: "var(--nx-error, #EF4444)", marginTop: 4, display: "block", fontFamily: "var(--nx-font-sans, system-ui, sans-serif)" }}>{error}</span>}
199
+ {open && (
200
+ <ul
201
+ id={`${id}-list`}
202
+ ref={listRef}
203
+ role="listbox"
204
+ style={{
205
+ position: "absolute",
206
+ top: "100%",
207
+ left: 0,
208
+ right: 0,
209
+ marginTop: 4,
210
+ background: "var(--nx-bg-elevated, #1C2128)",
211
+ border: "1px solid var(--nx-border-strong, rgba(255,255,255,0.16))",
212
+ borderRadius: 8,
213
+ boxShadow: "0 10px 15px rgba(0,0,0,0.50), 0 4px 6px rgba(0,0,0,0.30)",
214
+ padding: "4px 0",
215
+ zIndex: 50,
216
+ maxHeight: 240,
217
+ overflowY: "auto",
218
+ listStyle: "none",
219
+ margin: 0,
220
+ animation: "nx-slide-up 150ms ease forwards",
221
+ }}
222
+ >
223
+ {children}
224
+ </ul>
225
+ )}
226
+ </div>
227
+ </SelectContext.Provider>
228
+ );
229
+ }
230
+
231
+ interface SelectItemProps {
232
+ value: string;
233
+ children: ReactNode;
234
+ disabled?: boolean;
235
+ }
236
+
237
+ export function SelectItem({ value, children, disabled }: SelectItemProps) {
238
+ const ctx = useContext(SelectContext);
239
+ const label = typeof children === "string" ? children : value;
240
+
241
+ useEffect(() => { ctx.registerOption(value, label); }, [value, label]);
242
+
243
+ const enabledOptions = ctx.options.filter(o => !o.disabled);
244
+ const myIdx = enabledOptions.findIndex(o => o.value === value);
245
+ const isFocused = ctx.focusedIdx === myIdx;
246
+ const isSelected = ctx.value === value;
247
+
248
+ return (
249
+ <li
250
+ role="option"
251
+ aria-selected={isSelected}
252
+ aria-disabled={disabled}
253
+ onClick={() => !disabled && ctx.onChange(value, label)}
254
+ onMouseEnter={() => !disabled && ctx.setFocusedIdx(myIdx)}
255
+ style={{
256
+ padding: "7px 12px",
257
+ fontSize: "var(--nx-fs-sm, 12px)",
258
+ fontFamily: "var(--nx-font-sans, system-ui, sans-serif)",
259
+ color: disabled ? "var(--nx-text-4, #484F58)" : isSelected ? "var(--nx-accent-text, #F5B342)" : "var(--nx-text, #E6EDF3)",
260
+ background: isFocused ? "var(--nx-bg-overlay, #21262D)" : "transparent",
261
+ cursor: disabled ? "not-allowed" : "pointer",
262
+ display: "flex",
263
+ alignItems: "center",
264
+ justifyContent: "space-between",
265
+ gap: 8,
266
+ transition: "background 100ms ease",
267
+ }}
268
+ >
269
+ <span>{children}</span>
270
+ {isSelected && (
271
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
272
+ <path d="M20 6L9 17l-5-5"/>
273
+ </svg>
274
+ )}
275
+ </li>
276
+ );
277
+ }
278
+
279
+ export function SelectGroup({ label, children }: { label: string; children: ReactNode }) {
280
+ return (
281
+ <div>
282
+ <div style={{
283
+ padding: "6px 12px 2px",
284
+ fontSize: "10px",
285
+ fontWeight: 600,
286
+ letterSpacing: "0.06em",
287
+ textTransform: "uppercase",
288
+ color: "var(--nx-text-4, #484F58)",
289
+ fontFamily: "var(--nx-font-sans, system-ui, sans-serif)",
290
+ }}>
291
+ {label}
292
+ </div>
293
+ {children}
294
+ </div>
295
+ );
296
+ }
package/src/tabs.tsx ADDED
@@ -0,0 +1,150 @@
1
+ "use client";
2
+ // @yugnex/nexui-react — Tabs
3
+ // Controlled and uncontrolled usage.
4
+ // Usage:
5
+ // <Tabs defaultValue="code">
6
+ // <TabsList>
7
+ // <TabsTrigger value="code">Code</TabsTrigger>
8
+ // <TabsTrigger value="preview">Preview</TabsTrigger>
9
+ // </TabsList>
10
+ // <TabsContent value="code"><CodeView /></TabsContent>
11
+ // <TabsContent value="preview"><Preview /></TabsContent>
12
+ // </Tabs>
13
+
14
+ import React, {
15
+ createContext,
16
+ useContext,
17
+ useState,
18
+ useCallback,
19
+ type ReactNode,
20
+ type CSSProperties,
21
+ type KeyboardEvent,
22
+ } from "react";
23
+
24
+ interface TabsContextValue {
25
+ active: string;
26
+ setActive: (v: string) => void;
27
+ }
28
+ const TabsContext = createContext<TabsContextValue>({ active: "", setActive: () => {} });
29
+
30
+ interface TabsProps {
31
+ defaultValue?: string;
32
+ value?: string;
33
+ onChange?: (v: string) => void;
34
+ children?: ReactNode;
35
+ className?: string;
36
+ style?: CSSProperties;
37
+ }
38
+
39
+ export function Tabs({ defaultValue = "", value, onChange, children, className, style }: TabsProps) {
40
+ const [internal, setInternal] = useState(defaultValue);
41
+ const active = value !== undefined ? value : internal;
42
+ const setActive = useCallback((v: string) => {
43
+ setInternal(v);
44
+ onChange?.(v);
45
+ }, [onChange]);
46
+
47
+ return (
48
+ <TabsContext.Provider value={{ active, setActive }}>
49
+ <div className={className} style={{ display: "flex", flexDirection: "column", ...style }}>
50
+ {children}
51
+ </div>
52
+ </TabsContext.Provider>
53
+ );
54
+ }
55
+
56
+ interface TabsListProps {
57
+ children?: ReactNode;
58
+ className?: string;
59
+ style?: CSSProperties;
60
+ }
61
+
62
+ export function TabsList({ children, className, style }: TabsListProps) {
63
+ return (
64
+ <div
65
+ role="tablist"
66
+ className={className}
67
+ style={{
68
+ display: "flex",
69
+ alignItems: "center",
70
+ gap: 2,
71
+ borderBottom: "1px solid var(--nx-border, rgba(255,255,255,0.08))",
72
+ padding: "0 4px",
73
+ overflowX: "auto",
74
+ scrollbarWidth: "none",
75
+ ...style,
76
+ }}
77
+ >
78
+ {children}
79
+ </div>
80
+ );
81
+ }
82
+
83
+ interface TabsTriggerProps {
84
+ value: string;
85
+ children?: ReactNode;
86
+ disabled?: boolean;
87
+ className?: string;
88
+ style?: CSSProperties;
89
+ }
90
+
91
+ export function TabsTrigger({ value, children, disabled, className, style }: TabsTriggerProps) {
92
+ const { active, setActive } = useContext(TabsContext);
93
+ const isActive = active === value;
94
+
95
+ const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
96
+ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setActive(value); }
97
+ };
98
+
99
+ return (
100
+ <button
101
+ role="tab"
102
+ aria-selected={isActive}
103
+ tabIndex={isActive ? 0 : -1}
104
+ disabled={disabled}
105
+ className={className}
106
+ onClick={() => !disabled && setActive(value)}
107
+ onKeyDown={handleKeyDown}
108
+ style={{
109
+ display: "inline-flex",
110
+ alignItems: "center",
111
+ gap: 6,
112
+ padding: "8px 12px",
113
+ fontSize: "var(--nx-fs-sm, 12px)",
114
+ fontWeight: 500,
115
+ fontFamily: "var(--nx-font-sans, system-ui, sans-serif)",
116
+ color: isActive ? "var(--nx-text, #E6EDF3)" : "var(--nx-text-3, #6E7681)",
117
+ background: "transparent",
118
+ border: "none",
119
+ borderBottom: `2px solid ${isActive ? "var(--nx-accent, #E89010)" : "transparent"}`,
120
+ cursor: disabled ? "not-allowed" : "pointer",
121
+ opacity: disabled ? 0.4 : 1,
122
+ transition: "color 150ms ease, border-color 150ms ease",
123
+ whiteSpace: "nowrap",
124
+ marginBottom: -1,
125
+ outline: "none",
126
+ borderRadius: "4px 4px 0 0",
127
+ ...style,
128
+ }}
129
+ >
130
+ {children}
131
+ </button>
132
+ );
133
+ }
134
+
135
+ interface TabsContentProps {
136
+ value: string;
137
+ children?: ReactNode;
138
+ className?: string;
139
+ style?: CSSProperties;
140
+ }
141
+
142
+ export function TabsContent({ value, children, className, style }: TabsContentProps) {
143
+ const { active } = useContext(TabsContext);
144
+ if (active !== value) return null;
145
+ return (
146
+ <div role="tabpanel" className={className} style={{ flex: 1, ...style }}>
147
+ {children}
148
+ </div>
149
+ );
150
+ }
package/src/toast.tsx ADDED
@@ -0,0 +1,178 @@
1
+ "use client";
2
+ // @yugnex/nexui-react — Toast / Toaster
3
+ // Zero external deps. React context-based queue.
4
+ //
5
+ // Setup (app/layout.tsx):
6
+ // <NexuiProvider><Toaster />{children}</NexuiProvider>
7
+ //
8
+ // Usage anywhere:
9
+ // const { toast } = useToast();
10
+ // toast.success("Saved!");
11
+ // toast.error("Something went wrong");
12
+ // toast("Custom message", { variant: "accent", duration: 5000 });
13
+
14
+ import React, {
15
+ createContext,
16
+ useCallback,
17
+ useContext,
18
+ useEffect,
19
+ useId,
20
+ useRef,
21
+ useState,
22
+ type ReactNode,
23
+ type CSSProperties,
24
+ } from "react";
25
+
26
+ export type ToastVariant = "default" | "success" | "error" | "warning" | "accent" | "live";
27
+
28
+ export interface ToastItem {
29
+ id: string;
30
+ message: ReactNode;
31
+ variant: ToastVariant;
32
+ duration: number;
33
+ createdAt: number;
34
+ }
35
+
36
+ interface ToastContextValue {
37
+ toasts: ToastItem[];
38
+ add: (message: ReactNode, opts?: Partial<Pick<ToastItem, "variant" | "duration">>) => string;
39
+ remove: (id: string) => void;
40
+ }
41
+
42
+ const ToastContext = createContext<ToastContextValue>({
43
+ toasts: [],
44
+ add: () => "",
45
+ remove: () => {},
46
+ });
47
+
48
+ export function ToastProvider({ children }: { children: ReactNode }) {
49
+ const [toasts, setToasts] = useState<ToastItem[]>([]);
50
+ const counter = useRef(0);
51
+
52
+ const remove = useCallback((id: string) => {
53
+ setToasts(prev => prev.filter(t => t.id !== id));
54
+ }, []);
55
+
56
+ const add = useCallback((message: ReactNode, opts?: Partial<Pick<ToastItem, "variant" | "duration">>): string => {
57
+ const id = `nx-toast-${++counter.current}`;
58
+ const item: ToastItem = {
59
+ id,
60
+ message,
61
+ variant: opts?.variant ?? "default",
62
+ duration: opts?.duration ?? 4000,
63
+ createdAt: Date.now(),
64
+ };
65
+ setToasts(prev => [item, ...prev].slice(0, 5));
66
+ if (item.duration > 0) {
67
+ setTimeout(() => remove(id), item.duration);
68
+ }
69
+ return id;
70
+ }, [remove]);
71
+
72
+ return (
73
+ <ToastContext.Provider value={{ toasts, add, remove }}>
74
+ {children}
75
+ </ToastContext.Provider>
76
+ );
77
+ }
78
+
79
+ export function useToast() {
80
+ const { add, remove } = useContext(ToastContext);
81
+ return {
82
+ toast: Object.assign(
83
+ (msg: ReactNode, opts?: Partial<Pick<ToastItem, "variant" | "duration">>) => add(msg, opts),
84
+ {
85
+ success: (msg: ReactNode, opts?: Partial<Pick<ToastItem, "duration">>) => add(msg, { ...opts, variant: "success" }),
86
+ error: (msg: ReactNode, opts?: Partial<Pick<ToastItem, "duration">>) => add(msg, { ...opts, variant: "error" }),
87
+ warning: (msg: ReactNode, opts?: Partial<Pick<ToastItem, "duration">>) => add(msg, { ...opts, variant: "warning" }),
88
+ accent: (msg: ReactNode, opts?: Partial<Pick<ToastItem, "duration">>) => add(msg, { ...opts, variant: "accent" }),
89
+ live: (msg: ReactNode, opts?: Partial<Pick<ToastItem, "duration">>) => add(msg, { ...opts, variant: "live" }),
90
+ dismiss: remove,
91
+ }
92
+ ),
93
+ };
94
+ }
95
+
96
+ const VARIANT_STYLES: Record<ToastVariant, CSSProperties> = {
97
+ default: { borderColor: "var(--nx-border-strong, rgba(255,255,255,0.16))" },
98
+ success: { borderColor: "rgba(34,197,94,0.35)", color: "var(--nx-success, #22C55E)" },
99
+ error: { borderColor: "rgba(239,68,68,0.35)", color: "var(--nx-error, #EF4444)" },
100
+ warning: { borderColor: "rgba(234,179,8,0.35)", color: "var(--nx-warning, #EAB308)" },
101
+ accent: { borderColor: "var(--nx-accent-border, rgba(232,144,16,0.22))", color: "var(--nx-accent-text, #F5B342)" },
102
+ live: { borderColor: "var(--nx-live-border, rgba(15,212,198,0.22))", color: "var(--nx-live, #0FD4C6)" },
103
+ };
104
+
105
+ function ToastItem({ item, onClose }: { item: ToastItem; onClose: () => void }) {
106
+ const [visible, setVisible] = useState(false);
107
+ useEffect(() => { requestAnimationFrame(() => setVisible(true)); }, []);
108
+
109
+ return (
110
+ <div
111
+ role="alert"
112
+ aria-live="polite"
113
+ style={{
114
+ display: "flex",
115
+ alignItems: "flex-start",
116
+ justifyContent: "space-between",
117
+ gap: 12,
118
+ padding: "12px 16px",
119
+ background: "var(--nx-bg-elevated, #1C2128)",
120
+ border: "1px solid",
121
+ borderRadius: 8,
122
+ boxShadow: "0 10px 15px rgba(0,0,0,0.50), 0 4px 6px rgba(0,0,0,0.30)",
123
+ fontFamily: "var(--nx-font-sans, system-ui, sans-serif)",
124
+ fontSize: "var(--nx-fs-sm, 12px)",
125
+ color: "var(--nx-text, #E6EDF3)",
126
+ maxWidth: 380,
127
+ width: "100%",
128
+ transform: visible ? "translateY(0)" : "translateY(8px)",
129
+ opacity: visible ? 1 : 0,
130
+ transition: "transform 200ms ease, opacity 200ms ease",
131
+ ...VARIANT_STYLES[item.variant],
132
+ }}
133
+ >
134
+ <span style={{ flex: 1, lineHeight: 1.5 }}>{item.message}</span>
135
+ <button
136
+ onClick={onClose}
137
+ aria-label="Dismiss"
138
+ style={{
139
+ background: "none",
140
+ border: "none",
141
+ color: "var(--nx-text-4, #484F58)",
142
+ cursor: "pointer",
143
+ fontSize: 14,
144
+ lineHeight: 1,
145
+ padding: 0,
146
+ flexShrink: 0,
147
+ marginTop: 1,
148
+ }}
149
+ >✕</button>
150
+ </div>
151
+ );
152
+ }
153
+
154
+ export function Toaster() {
155
+ const { toasts, remove } = useContext(ToastContext);
156
+
157
+ return (
158
+ <div
159
+ aria-label="Notifications"
160
+ style={{
161
+ position: "fixed",
162
+ bottom: 24,
163
+ right: 24,
164
+ zIndex: 300,
165
+ display: "flex",
166
+ flexDirection: "column",
167
+ gap: 8,
168
+ pointerEvents: "none",
169
+ }}
170
+ >
171
+ {toasts.map(t => (
172
+ <div key={t.id} style={{ pointerEvents: "auto" }}>
173
+ <ToastItem item={t} onClose={() => remove(t.id)} />
174
+ </div>
175
+ ))}
176
+ </div>
177
+ );
178
+ }