@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.
- package/dist/card.d.ts +31 -0
- package/dist/card.d.ts.map +1 -0
- package/dist/card.js +73 -0
- package/dist/card.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/modal.d.ts +13 -0
- package/dist/modal.d.ts.map +1 -0
- package/dist/modal.js +117 -0
- package/dist/modal.js.map +1 -0
- package/dist/primitives.d.ts +162 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +100 -0
- package/dist/primitives.js.map +1 -0
- package/dist/provider.d.ts +16 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +58 -0
- package/dist/provider.js.map +1 -0
- package/dist/select.d.ts +27 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +172 -0
- package/dist/select.js.map +1 -0
- package/dist/tabs.d.ts +33 -0
- package/dist/tabs.d.ts.map +1 -0
- package/dist/tabs.js +74 -0
- package/dist/tabs.js.map +1 -0
- package/dist/toast.d.ts +24 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/toast.js +111 -0
- package/dist/toast.js.map +1 -0
- package/dist/tooltip.d.ts +12 -0
- package/dist/tooltip.d.ts.map +1 -0
- package/dist/tooltip.js +52 -0
- package/dist/tooltip.js.map +1 -0
- package/package.json +44 -0
- package/src/card.tsx +137 -0
- package/src/index.ts +35 -0
- package/src/modal.tsx +172 -0
- package/src/primitives.tsx +344 -0
- package/src/provider.tsx +88 -0
- package/src/select.tsx +296 -0
- package/src/tabs.tsx +150 -0
- package/src/toast.tsx +178 -0
- package/src/tooltip.tsx +90 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// @yugnex/nexui-react — Web Component Wrappers
|
|
3
|
+
// Thin typed React wrappers for every @yugnex/nexui Web Component.
|
|
4
|
+
// Each wrapper:
|
|
5
|
+
// • Declares proper TypeScript prop types
|
|
6
|
+
// • Forwards refs to the underlying DOM element
|
|
7
|
+
// • Handles imperative APIs (e.g. TextStream.pushLogTrace)
|
|
8
|
+
// • Bridges React synthetic events ↔ Web Component custom events
|
|
9
|
+
|
|
10
|
+
import React, {
|
|
11
|
+
forwardRef,
|
|
12
|
+
useEffect,
|
|
13
|
+
useImperativeHandle,
|
|
14
|
+
useRef,
|
|
15
|
+
type ReactNode,
|
|
16
|
+
} from "react";
|
|
17
|
+
|
|
18
|
+
// ─── JSX type augmentation so nex-* tags work in TSX ──────────────────────
|
|
19
|
+
declare global {
|
|
20
|
+
namespace JSX {
|
|
21
|
+
interface IntrinsicElements {
|
|
22
|
+
"nex-panel": any;
|
|
23
|
+
"nex-button": any;
|
|
24
|
+
"nex-badge": any;
|
|
25
|
+
"nex-input": any;
|
|
26
|
+
"nex-avatar": any;
|
|
27
|
+
"nex-status-ring": any;
|
|
28
|
+
"nex-text-stream": any;
|
|
29
|
+
"nex-progress": any;
|
|
30
|
+
"nex-switch": any;
|
|
31
|
+
"nex-checkbox": any;
|
|
32
|
+
"nex-skeleton": any;
|
|
33
|
+
"nex-separator": any;
|
|
34
|
+
"nex-spinner": any;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Panel ────────────────────────────────────────────────────────────────
|
|
40
|
+
interface NexPanelProps {
|
|
41
|
+
variant?: "void" | "base" | "surface" | "elevated" | "overlay" | "accent" | "live" | "success" | "error" | "warning";
|
|
42
|
+
padding?: "none" | "xs" | "sm" | "md" | "lg" | "xl";
|
|
43
|
+
elevation?: "0" | "1" | "2" | "3" | "4";
|
|
44
|
+
matrix?: string;
|
|
45
|
+
className?: string;
|
|
46
|
+
children?: ReactNode;
|
|
47
|
+
style?: React.CSSProperties;
|
|
48
|
+
}
|
|
49
|
+
export const Panel = forwardRef<HTMLElement, NexPanelProps>(
|
|
50
|
+
({ children, ...props }, ref) => (
|
|
51
|
+
<nex-panel ref={ref as any} {...props}>{children}</nex-panel>
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
Panel.displayName = "Panel";
|
|
55
|
+
|
|
56
|
+
// ─── Button ───────────────────────────────────────────────────────────────
|
|
57
|
+
interface NexButtonProps {
|
|
58
|
+
variant?: "primary" | "secondary" | "ghost" | "danger" | "outline" | "accent" | "live";
|
|
59
|
+
size?: "sm" | "md" | "lg";
|
|
60
|
+
loading?: boolean;
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
"icon-only"?: boolean;
|
|
63
|
+
type?: "button" | "submit" | "reset";
|
|
64
|
+
className?: string;
|
|
65
|
+
children?: ReactNode;
|
|
66
|
+
onClick?: React.MouseEventHandler<HTMLElement>;
|
|
67
|
+
style?: React.CSSProperties;
|
|
68
|
+
}
|
|
69
|
+
export const Button = forwardRef<HTMLElement, NexButtonProps>(
|
|
70
|
+
({ loading, "icon-only": iconOnly, children, ...props }, ref) => (
|
|
71
|
+
<nex-button
|
|
72
|
+
ref={ref as any}
|
|
73
|
+
loading={loading ? "true" : undefined}
|
|
74
|
+
icon-only={iconOnly ? "true" : undefined}
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
{children}
|
|
78
|
+
</nex-button>
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
Button.displayName = "Button";
|
|
82
|
+
|
|
83
|
+
// ─── Badge ────────────────────────────────────────────────────────────────
|
|
84
|
+
interface NexBadgeProps {
|
|
85
|
+
variant?: "default" | "accent" | "live" | "success" | "error" | "warning" | "muted";
|
|
86
|
+
size?: "sm" | "md";
|
|
87
|
+
dot?: boolean;
|
|
88
|
+
className?: string;
|
|
89
|
+
children?: ReactNode;
|
|
90
|
+
style?: React.CSSProperties;
|
|
91
|
+
}
|
|
92
|
+
export const Badge = forwardRef<HTMLElement, NexBadgeProps>(
|
|
93
|
+
({ dot, children, ...props }, ref) => (
|
|
94
|
+
<nex-badge ref={ref as any} dot={dot ? "true" : undefined} {...props}>
|
|
95
|
+
{children}
|
|
96
|
+
</nex-badge>
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
Badge.displayName = "Badge";
|
|
100
|
+
|
|
101
|
+
// ─── Input ────────────────────────────────────────────────────────────────
|
|
102
|
+
interface NexInputProps {
|
|
103
|
+
type?: string;
|
|
104
|
+
label?: string;
|
|
105
|
+
placeholder?: string;
|
|
106
|
+
helper?: string;
|
|
107
|
+
error?: string;
|
|
108
|
+
size?: "sm" | "md" | "lg";
|
|
109
|
+
disabled?: boolean;
|
|
110
|
+
required?: boolean;
|
|
111
|
+
value?: string;
|
|
112
|
+
className?: string;
|
|
113
|
+
onChange?: React.ChangeEventHandler<HTMLInputElement>;
|
|
114
|
+
onInput?: React.FormEventHandler<HTMLInputElement>;
|
|
115
|
+
onFocus?: React.FocusEventHandler<HTMLInputElement>;
|
|
116
|
+
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
|
117
|
+
style?: React.CSSProperties;
|
|
118
|
+
}
|
|
119
|
+
export const Input = forwardRef<HTMLElement, NexInputProps>(
|
|
120
|
+
({ onChange, onInput, onFocus, onBlur, ...props }, ref) => {
|
|
121
|
+
const elRef = useRef<HTMLElement>(null);
|
|
122
|
+
useImperativeHandle(ref, () => elRef.current!);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const el = elRef.current;
|
|
126
|
+
if (!el) return;
|
|
127
|
+
const handlers: Array<[string, EventListener]> = [];
|
|
128
|
+
if (onChange) { const h = (e: Event) => onChange(e as any); el.addEventListener("change", h); handlers.push(["change", h]); }
|
|
129
|
+
if (onInput) { const h = (e: Event) => onInput(e as any); el.addEventListener("input", h); handlers.push(["input", h]); }
|
|
130
|
+
if (onFocus) { const h = (e: Event) => onFocus(e as any); el.addEventListener("focus", h); handlers.push(["focus", h]); }
|
|
131
|
+
if (onBlur) { const h = (e: Event) => onBlur(e as any); el.addEventListener("blur", h); handlers.push(["blur", h]); }
|
|
132
|
+
return () => handlers.forEach(([ev, h]) => el.removeEventListener(ev, h));
|
|
133
|
+
}, [onChange, onInput, onFocus, onBlur]);
|
|
134
|
+
|
|
135
|
+
return <nex-input ref={elRef as any} {...props} />;
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
Input.displayName = "Input";
|
|
139
|
+
|
|
140
|
+
// ─── Avatar ───────────────────────────────────────────────────────────────
|
|
141
|
+
interface NexAvatarProps {
|
|
142
|
+
src?: string;
|
|
143
|
+
name?: string;
|
|
144
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
145
|
+
status?: "online" | "away" | "busy" | "offline";
|
|
146
|
+
shape?: "circle" | "square";
|
|
147
|
+
className?: string;
|
|
148
|
+
style?: React.CSSProperties;
|
|
149
|
+
}
|
|
150
|
+
export const Avatar = forwardRef<HTMLElement, NexAvatarProps>(
|
|
151
|
+
(props, ref) => <nex-avatar ref={ref as any} {...props} />
|
|
152
|
+
);
|
|
153
|
+
Avatar.displayName = "Avatar";
|
|
154
|
+
|
|
155
|
+
// ─── StatusRing ───────────────────────────────────────────────────────────
|
|
156
|
+
interface NexStatusRingProps {
|
|
157
|
+
score?: number;
|
|
158
|
+
label?: string;
|
|
159
|
+
color?: string;
|
|
160
|
+
size?: number;
|
|
161
|
+
className?: string;
|
|
162
|
+
style?: React.CSSProperties;
|
|
163
|
+
}
|
|
164
|
+
export const StatusRing = forwardRef<HTMLElement, NexStatusRingProps>(
|
|
165
|
+
({ score, size, ...props }, ref) => (
|
|
166
|
+
<nex-status-ring
|
|
167
|
+
ref={ref as any}
|
|
168
|
+
score={score !== undefined ? String(score) : undefined}
|
|
169
|
+
size={size !== undefined ? String(size) : undefined}
|
|
170
|
+
{...props}
|
|
171
|
+
/>
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
StatusRing.displayName = "StatusRing";
|
|
175
|
+
|
|
176
|
+
// ─── TextStream — imperative ref API ──────────────────────────────────────
|
|
177
|
+
export interface TextStreamHandle {
|
|
178
|
+
push: (msg: string) => void;
|
|
179
|
+
batch: (msgs: string[]) => void;
|
|
180
|
+
clear: () => void;
|
|
181
|
+
}
|
|
182
|
+
interface NexTextStreamProps {
|
|
183
|
+
maxRows?: number;
|
|
184
|
+
showNumbers?: boolean;
|
|
185
|
+
className?: string;
|
|
186
|
+
style?: React.CSSProperties;
|
|
187
|
+
}
|
|
188
|
+
export const TextStream = forwardRef<TextStreamHandle, NexTextStreamProps>(
|
|
189
|
+
({ maxRows, showNumbers, ...props }, ref) => {
|
|
190
|
+
const elRef = useRef<any>(null);
|
|
191
|
+
useImperativeHandle(ref, () => ({
|
|
192
|
+
push: (msg: string) => elRef.current?.pushLogTrace(msg),
|
|
193
|
+
batch: (msgs: string[]) => elRef.current?.pushBatch(msgs),
|
|
194
|
+
clear: () => elRef.current?.clear(),
|
|
195
|
+
}));
|
|
196
|
+
return (
|
|
197
|
+
<nex-text-stream
|
|
198
|
+
ref={elRef}
|
|
199
|
+
max-rows={maxRows !== undefined ? String(maxRows) : undefined}
|
|
200
|
+
show-numbers={showNumbers === false ? "false" : undefined}
|
|
201
|
+
{...props}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
TextStream.displayName = "TextStream";
|
|
207
|
+
|
|
208
|
+
// ─── Progress ─────────────────────────────────────────────────────────────
|
|
209
|
+
interface NexProgressProps {
|
|
210
|
+
value?: number;
|
|
211
|
+
variant?: "linear" | "circular";
|
|
212
|
+
size?: "sm" | "md" | "lg";
|
|
213
|
+
color?: "accent" | "live" | "success" | "error" | "warning";
|
|
214
|
+
label?: string;
|
|
215
|
+
"show-value"?: boolean;
|
|
216
|
+
className?: string;
|
|
217
|
+
style?: React.CSSProperties;
|
|
218
|
+
}
|
|
219
|
+
export const Progress = forwardRef<HTMLElement, NexProgressProps>(
|
|
220
|
+
({ value, "show-value": showVal, ...props }, ref) => (
|
|
221
|
+
<nex-progress
|
|
222
|
+
ref={ref as any}
|
|
223
|
+
value={value !== undefined ? String(value) : undefined}
|
|
224
|
+
show-value={showVal ? "true" : undefined}
|
|
225
|
+
{...props}
|
|
226
|
+
/>
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
Progress.displayName = "Progress";
|
|
230
|
+
|
|
231
|
+
// ─── Switch ───────────────────────────────────────────────────────────────
|
|
232
|
+
interface NexSwitchProps {
|
|
233
|
+
checked?: boolean;
|
|
234
|
+
disabled?: boolean;
|
|
235
|
+
label?: string;
|
|
236
|
+
size?: "sm" | "md" | "lg";
|
|
237
|
+
color?: "accent" | "live" | "success";
|
|
238
|
+
className?: string;
|
|
239
|
+
onChange?: (checked: boolean) => void;
|
|
240
|
+
style?: React.CSSProperties;
|
|
241
|
+
}
|
|
242
|
+
export const Switch = forwardRef<HTMLElement, NexSwitchProps>(
|
|
243
|
+
({ checked, onChange, ...props }, ref) => {
|
|
244
|
+
const elRef = useRef<HTMLElement>(null);
|
|
245
|
+
useImperativeHandle(ref, () => elRef.current!);
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
const el = elRef.current;
|
|
248
|
+
if (!el || !onChange) return;
|
|
249
|
+
const h = (e: Event) => onChange((e as CustomEvent).detail.checked);
|
|
250
|
+
el.addEventListener("change", h);
|
|
251
|
+
return () => el.removeEventListener("change", h);
|
|
252
|
+
}, [onChange]);
|
|
253
|
+
return (
|
|
254
|
+
<nex-switch
|
|
255
|
+
ref={elRef as any}
|
|
256
|
+
checked={checked ? "" : undefined}
|
|
257
|
+
{...props}
|
|
258
|
+
/>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
Switch.displayName = "Switch";
|
|
263
|
+
|
|
264
|
+
// ─── Checkbox ─────────────────────────────────────────────────────────────
|
|
265
|
+
interface NexCheckboxProps {
|
|
266
|
+
checked?: boolean;
|
|
267
|
+
indeterminate?: boolean;
|
|
268
|
+
disabled?: boolean;
|
|
269
|
+
label?: string;
|
|
270
|
+
size?: "sm" | "md" | "lg";
|
|
271
|
+
className?: string;
|
|
272
|
+
onChange?: (checked: boolean) => void;
|
|
273
|
+
style?: React.CSSProperties;
|
|
274
|
+
}
|
|
275
|
+
export const Checkbox = forwardRef<HTMLElement, NexCheckboxProps>(
|
|
276
|
+
({ checked, indeterminate, onChange, ...props }, ref) => {
|
|
277
|
+
const elRef = useRef<HTMLElement>(null);
|
|
278
|
+
useImperativeHandle(ref, () => elRef.current!);
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
const el = elRef.current;
|
|
281
|
+
if (!el || !onChange) return;
|
|
282
|
+
const h = (e: Event) => onChange((e as CustomEvent).detail.checked);
|
|
283
|
+
el.addEventListener("change", h);
|
|
284
|
+
return () => el.removeEventListener("change", h);
|
|
285
|
+
}, [onChange]);
|
|
286
|
+
return (
|
|
287
|
+
<nex-checkbox
|
|
288
|
+
ref={elRef as any}
|
|
289
|
+
checked={checked ? "" : undefined}
|
|
290
|
+
indeterminate={indeterminate ? "" : undefined}
|
|
291
|
+
{...props}
|
|
292
|
+
/>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
Checkbox.displayName = "Checkbox";
|
|
297
|
+
|
|
298
|
+
// ─── Skeleton ─────────────────────────────────────────────────────────────
|
|
299
|
+
interface NexSkeletonProps {
|
|
300
|
+
variant?: "text" | "circle" | "rect";
|
|
301
|
+
width?: string;
|
|
302
|
+
height?: string;
|
|
303
|
+
lines?: number;
|
|
304
|
+
animate?: boolean;
|
|
305
|
+
className?: string;
|
|
306
|
+
style?: React.CSSProperties;
|
|
307
|
+
}
|
|
308
|
+
export const Skeleton = forwardRef<HTMLElement, NexSkeletonProps>(
|
|
309
|
+
({ lines, animate, ...props }, ref) => (
|
|
310
|
+
<nex-skeleton
|
|
311
|
+
ref={ref as any}
|
|
312
|
+
lines={lines !== undefined ? String(lines) : undefined}
|
|
313
|
+
animate={animate === false ? "false" : undefined}
|
|
314
|
+
{...props}
|
|
315
|
+
/>
|
|
316
|
+
)
|
|
317
|
+
);
|
|
318
|
+
Skeleton.displayName = "Skeleton";
|
|
319
|
+
|
|
320
|
+
// ─── Separator ────────────────────────────────────────────────────────────
|
|
321
|
+
interface NexSeparatorProps {
|
|
322
|
+
orientation?: "horizontal" | "vertical";
|
|
323
|
+
variant?: "default" | "strong" | "muted";
|
|
324
|
+
label?: string;
|
|
325
|
+
className?: string;
|
|
326
|
+
style?: React.CSSProperties;
|
|
327
|
+
}
|
|
328
|
+
export const Separator = forwardRef<HTMLElement, NexSeparatorProps>(
|
|
329
|
+
(props, ref) => <nex-separator ref={ref as any} {...props} />
|
|
330
|
+
);
|
|
331
|
+
Separator.displayName = "Separator";
|
|
332
|
+
|
|
333
|
+
// ─── Spinner ──────────────────────────────────────────────────────────────
|
|
334
|
+
interface NexSpinnerProps {
|
|
335
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
336
|
+
color?: "accent" | "live" | "success" | "error" | "muted";
|
|
337
|
+
label?: string;
|
|
338
|
+
className?: string;
|
|
339
|
+
style?: React.CSSProperties;
|
|
340
|
+
}
|
|
341
|
+
export const Spinner = forwardRef<HTMLElement, NexSpinnerProps>(
|
|
342
|
+
(props, ref) => <nex-spinner ref={ref as any} {...props} />
|
|
343
|
+
);
|
|
344
|
+
Spinner.displayName = "Spinner";
|
package/src/provider.tsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// @yugnex/nexui-react — NexuiProvider
|
|
3
|
+
// Wraps your Next.js app. Initializes the Web Component engine client-side.
|
|
4
|
+
// SSR-safe: the engine call is gated behind useEffect (browser-only).
|
|
5
|
+
//
|
|
6
|
+
// Usage in app/layout.tsx:
|
|
7
|
+
// import { NexuiProvider } from "@yugnex/nexui-react";
|
|
8
|
+
// export default function RootLayout({ children }) {
|
|
9
|
+
// return <html><body><NexuiProvider theme="void">{children}</NexuiProvider></body></html>;
|
|
10
|
+
// }
|
|
11
|
+
|
|
12
|
+
import React, {
|
|
13
|
+
createContext,
|
|
14
|
+
useContext,
|
|
15
|
+
useEffect,
|
|
16
|
+
useState,
|
|
17
|
+
useCallback,
|
|
18
|
+
type ReactNode,
|
|
19
|
+
} from "react";
|
|
20
|
+
|
|
21
|
+
export type NexuiTheme = "void" | "terminal";
|
|
22
|
+
|
|
23
|
+
interface NexuiContextValue {
|
|
24
|
+
theme: NexuiTheme;
|
|
25
|
+
setTheme: (t: NexuiTheme) => void;
|
|
26
|
+
ready: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const NexuiContext = createContext<NexuiContextValue>({
|
|
30
|
+
theme: "void",
|
|
31
|
+
setTheme: () => {},
|
|
32
|
+
ready: false,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export function useNexui() {
|
|
36
|
+
return useContext(NexuiContext);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface NexuiProviderProps {
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
theme?: NexuiTheme;
|
|
42
|
+
onThemeChange?: (t: NexuiTheme) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function NexuiProvider({ children, theme: initialTheme = "void", onThemeChange }: NexuiProviderProps) {
|
|
46
|
+
const [theme, setThemeState] = useState<NexuiTheme>(initialTheme);
|
|
47
|
+
const [ready, setReady] = useState(false);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
let cancelled = false;
|
|
51
|
+
(async () => {
|
|
52
|
+
const { initializeNexuiEngine } = await import("@yugnex/nexui");
|
|
53
|
+
await initializeNexuiEngine(theme);
|
|
54
|
+
if (!cancelled) setReady(true);
|
|
55
|
+
})();
|
|
56
|
+
return () => { cancelled = true; };
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!ready) return;
|
|
61
|
+
(async () => {
|
|
62
|
+
const { setNexuiTheme } = await import("@yugnex/nexui");
|
|
63
|
+
setNexuiTheme(theme);
|
|
64
|
+
})();
|
|
65
|
+
}, [theme, ready]);
|
|
66
|
+
|
|
67
|
+
// Listen for programmatic theme changes from outside React
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const handler = (e: Event) => {
|
|
70
|
+
const t = (e as CustomEvent<{ theme: NexuiTheme }>).detail.theme;
|
|
71
|
+
setThemeState(t);
|
|
72
|
+
onThemeChange?.(t);
|
|
73
|
+
};
|
|
74
|
+
window.addEventListener("nexui:theme-change", handler);
|
|
75
|
+
return () => window.removeEventListener("nexui:theme-change", handler);
|
|
76
|
+
}, [onThemeChange]);
|
|
77
|
+
|
|
78
|
+
const setTheme = useCallback((t: NexuiTheme) => {
|
|
79
|
+
setThemeState(t);
|
|
80
|
+
onThemeChange?.(t);
|
|
81
|
+
}, [onThemeChange]);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<NexuiContext.Provider value={{ theme, setTheme, ready }}>
|
|
85
|
+
{children}
|
|
86
|
+
</NexuiContext.Provider>
|
|
87
|
+
);
|
|
88
|
+
}
|