@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 @@
|
|
|
1
|
+
{"version":3,"file":"tooltip.js","sourceRoot":"","sources":["../src/tooltip.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AACb,gCAAgC;AAChC,uDAAuD;AACvD,8EAA8E;AAE9E,OAAO,KAAK,EAAE,EACZ,QAAQ,EACR,MAAM,EACN,WAAW,GAGZ,MAAM,OAAO,CAAC;AAYf,MAAM,WAAW,GAA6B;IAC5C,GAAG,EAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE;IAClF,MAAM,EAAE,EAAE,GAAG,EAAK,kBAAkB,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE;IAClF,IAAI,EAAI,EAAE,KAAK,EAAG,kBAAkB,EAAE,GAAG,EAAG,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE;IAClF,KAAK,EAAG,EAAE,IAAI,EAAI,kBAAkB,EAAE,GAAG,EAAG,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE;CACnF,CAAC;AAEF,MAAM,UAAU,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,GAAG,KAAK,EAAE,KAAK,GAAG,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAgB;IAC9F,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAO,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClD,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAK,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClD,MAAM,SAAS,GAAG,MAAM,EAAiC,CAAC;IAC1D,MAAM,SAAS,GAAG,MAAM,EAAiC,CAAC;IAE1D,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5B,YAAY,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAChC,SAAS,CAAC,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAClC,UAAU,CAAC,IAAI,CAAC,CAAC;YACjB,qBAAqB,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;QACjD,CAAC,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAEZ,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5B,YAAY,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAChC,WAAW,CAAC,KAAK,CAAC,CAAC;QACnB,SAAS,CAAC,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC;IAC/D,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,IAAI,QAAQ;QAAE,OAAO,4BAAG,QAAQ,GAAI,CAAC;IAErC,OAAO,CACL,gBACE,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,EACvD,YAAY,EAAE,IAAI,EAClB,YAAY,EAAE,IAAI,EAClB,OAAO,EAAE,IAAI,EACb,MAAM,EAAE,IAAI,aAEX,QAAQ,EACR,OAAO,IAAI,CACV,eACE,IAAI,EAAC,SAAS,EACd,KAAK,EAAE;oBACL,QAAQ,EAAE,UAAU;oBACpB,MAAM,EAAE,GAAG;oBACX,UAAU,EAAE,+BAA+B;oBAC3C,MAAM,EAAE,2DAA2D;oBACnE,YAAY,EAAE,CAAC;oBACf,OAAO,EAAE,UAAU;oBACnB,QAAQ,EAAE,uBAAuB;oBACjC,UAAU,EAAE,4CAA4C;oBACxD,UAAU,EAAE,GAAG;oBACf,KAAK,EAAE,yBAAyB;oBAChC,UAAU,EAAE,QAAQ;oBACpB,SAAS,EAAE,4BAA4B;oBACvC,aAAa,EAAE,MAAM;oBACrB,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;oBACzB,SAAS,EAAE,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,SAAS,IAAI,EAAE,UAAU,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG;oBAC/E,UAAU,EAAE,0CAA0C;oBACtD,GAAG,WAAW,CAAC,IAAI,CAAC;iBACrB,YAEA,OAAO,GACH,CACR,IACI,CACR,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yugnex/nexui-react",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "React component library for @yugnex/nexui. Full replacement for Tailwind CSS + MUI + shadcn/ui. Zero external dependencies.",
|
|
5
|
+
"license": "LicenseRef-YugNex-Proprietary",
|
|
6
|
+
"author": "YugNex Technology (OPC) Private Limited <legal@yugnex.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./provider": { "import": "./dist/provider.js", "types": "./dist/provider.d.ts" },
|
|
17
|
+
"./toast": { "import": "./dist/toast.js", "types": "./dist/toast.d.ts" },
|
|
18
|
+
"./modal": { "import": "./dist/modal.js", "types": "./dist/modal.d.ts" },
|
|
19
|
+
"./tabs": { "import": "./dist/tabs.js", "types": "./dist/tabs.d.ts" },
|
|
20
|
+
"./select": { "import": "./dist/select.js", "types": "./dist/select.d.ts" },
|
|
21
|
+
"./tooltip": { "import": "./dist/tooltip.js", "types": "./dist/tooltip.d.ts" },
|
|
22
|
+
"./card": { "import": "./dist/card.js", "types": "./dist/card.d.ts" },
|
|
23
|
+
"./primitives": { "import": "./dist/primitives.js", "types": "./dist/primitives.d.ts" }
|
|
24
|
+
},
|
|
25
|
+
"files": ["dist", "src"],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc --project tsconfig.json",
|
|
28
|
+
"build:watch": "tsc --project tsconfig.json --watch",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"clean": "rm -rf dist"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": ">=18.0.0",
|
|
34
|
+
"react-dom": ">=18.0.0",
|
|
35
|
+
"@yugnex/nexui": "^2.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.5.0",
|
|
39
|
+
"@types/react": "^18.0.0",
|
|
40
|
+
"@types/react-dom": "^18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {},
|
|
43
|
+
"sideEffects": false
|
|
44
|
+
}
|
package/src/card.tsx
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// @yugnex/nexui-react — Card
|
|
3
|
+
// Composable card with optional header, body, and footer sections.
|
|
4
|
+
// Usage:
|
|
5
|
+
// <Card>
|
|
6
|
+
// <CardHeader title="Project" action={<Badge>Active</Badge>} />
|
|
7
|
+
// <CardBody>Content here</CardBody>
|
|
8
|
+
// <CardFooter><Button>Open</Button></CardFooter>
|
|
9
|
+
// </Card>
|
|
10
|
+
|
|
11
|
+
import React, { type ReactNode, type CSSProperties } from "react";
|
|
12
|
+
|
|
13
|
+
interface CardProps {
|
|
14
|
+
variant?: "base" | "surface" | "elevated";
|
|
15
|
+
hoverable?: boolean;
|
|
16
|
+
onClick?: () => void;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Card({ variant = "surface", hoverable, onClick, children, className, style }: CardProps) {
|
|
23
|
+
const bgMap: Record<string, string> = {
|
|
24
|
+
base: "var(--nx-bg-base, #0D1117)",
|
|
25
|
+
surface: "var(--nx-bg-surface, #161B22)",
|
|
26
|
+
elevated: "var(--nx-bg-elevated, #1C2128)",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className={className}
|
|
32
|
+
onClick={onClick}
|
|
33
|
+
role={onClick ? "button" : undefined}
|
|
34
|
+
tabIndex={onClick ? 0 : undefined}
|
|
35
|
+
style={{
|
|
36
|
+
background: bgMap[variant] ?? bgMap.surface,
|
|
37
|
+
border: "1px solid var(--nx-border, rgba(255,255,255,0.08))",
|
|
38
|
+
borderRadius: 10,
|
|
39
|
+
overflow: "hidden",
|
|
40
|
+
fontFamily: "var(--nx-font-sans, system-ui, sans-serif)",
|
|
41
|
+
color: "var(--nx-text, #E6EDF3)",
|
|
42
|
+
cursor: onClick ? "pointer" : "default",
|
|
43
|
+
transition: hoverable || onClick ? "border-color 150ms ease, box-shadow 150ms ease, transform 150ms ease" : "none",
|
|
44
|
+
...(hoverable || onClick ? {
|
|
45
|
+
["&:hover" as string]: {
|
|
46
|
+
borderColor: "var(--nx-border-strong, rgba(255,255,255,0.16))",
|
|
47
|
+
boxShadow: "0 4px 6px rgba(0,0,0,0.40)",
|
|
48
|
+
transform: "translateY(-1px)",
|
|
49
|
+
},
|
|
50
|
+
} : {}),
|
|
51
|
+
...style,
|
|
52
|
+
}}
|
|
53
|
+
onMouseEnter={hoverable || onClick ? e => {
|
|
54
|
+
const el = e.currentTarget as HTMLDivElement;
|
|
55
|
+
el.style.borderColor = "var(--nx-border-strong, rgba(255,255,255,0.16))";
|
|
56
|
+
el.style.boxShadow = "0 4px 6px rgba(0,0,0,0.40)";
|
|
57
|
+
if (onClick) el.style.transform = "translateY(-1px)";
|
|
58
|
+
} : undefined}
|
|
59
|
+
onMouseLeave={hoverable || onClick ? e => {
|
|
60
|
+
const el = e.currentTarget as HTMLDivElement;
|
|
61
|
+
el.style.borderColor = "";
|
|
62
|
+
el.style.boxShadow = "";
|
|
63
|
+
el.style.transform = "";
|
|
64
|
+
} : undefined}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface CardHeaderProps {
|
|
72
|
+
title?: ReactNode;
|
|
73
|
+
subtitle?: ReactNode;
|
|
74
|
+
action?: ReactNode;
|
|
75
|
+
children?: ReactNode;
|
|
76
|
+
className?: string;
|
|
77
|
+
style?: CSSProperties;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function CardHeader({ title, subtitle, action, children, className, style }: CardHeaderProps) {
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
className={className}
|
|
84
|
+
style={{
|
|
85
|
+
display: "flex",
|
|
86
|
+
alignItems: "flex-start",
|
|
87
|
+
justifyContent: "space-between",
|
|
88
|
+
gap: 12,
|
|
89
|
+
padding: "16px 20px",
|
|
90
|
+
borderBottom: "1px solid var(--nx-border, rgba(255,255,255,0.08))",
|
|
91
|
+
...style,
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
95
|
+
{title && (
|
|
96
|
+
<div style={{ fontSize: "var(--nx-fs-md, 14px)", fontWeight: 600, color: "var(--nx-text, #E6EDF3)", lineHeight: 1.3 }}>
|
|
97
|
+
{title}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
{subtitle && (
|
|
101
|
+
<div style={{ fontSize: "var(--nx-fs-xs, 11px)", color: "var(--nx-text-3, #6E7681)", marginTop: 3, lineHeight: 1.4 }}>
|
|
102
|
+
{subtitle}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
{children}
|
|
106
|
+
</div>
|
|
107
|
+
{action && <div style={{ flexShrink: 0 }}>{action}</div>}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function CardBody({ children, className, style }: { children?: ReactNode; className?: string; style?: CSSProperties }) {
|
|
113
|
+
return (
|
|
114
|
+
<div className={className} style={{ padding: "16px 20px", ...style }}>
|
|
115
|
+
{children}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function CardFooter({ children, className, style }: { children?: ReactNode; className?: string; style?: CSSProperties }) {
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
className={className}
|
|
124
|
+
style={{
|
|
125
|
+
display: "flex",
|
|
126
|
+
alignItems: "center",
|
|
127
|
+
justifyContent: "flex-end",
|
|
128
|
+
gap: 8,
|
|
129
|
+
padding: "12px 20px",
|
|
130
|
+
borderTop: "1px solid var(--nx-border, rgba(255,255,255,0.08))",
|
|
131
|
+
...style,
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{children}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @yugnex/nexui-react — Main Entry Point
|
|
2
|
+
// Full React component library powered by @yugnex/nexui Web Components.
|
|
3
|
+
// Zero external dependencies beyond React itself.
|
|
4
|
+
|
|
5
|
+
// ── Provider & Hooks ─────────────────────────────────────────────────────────
|
|
6
|
+
export { NexuiProvider, useNexui, type NexuiTheme } from "./provider";
|
|
7
|
+
export { ToastProvider, Toaster, useToast } from "./toast";
|
|
8
|
+
|
|
9
|
+
// ── Web Component Wrappers ───────────────────────────────────────────────────
|
|
10
|
+
export {
|
|
11
|
+
Panel,
|
|
12
|
+
Button,
|
|
13
|
+
Badge,
|
|
14
|
+
Input,
|
|
15
|
+
Avatar,
|
|
16
|
+
StatusRing,
|
|
17
|
+
TextStream,
|
|
18
|
+
Progress,
|
|
19
|
+
Switch,
|
|
20
|
+
Checkbox,
|
|
21
|
+
Skeleton,
|
|
22
|
+
Separator,
|
|
23
|
+
Spinner,
|
|
24
|
+
type TextStreamHandle,
|
|
25
|
+
} from "./primitives";
|
|
26
|
+
|
|
27
|
+
// ── React-Native Components ──────────────────────────────────────────────────
|
|
28
|
+
export { Modal } from "./modal";
|
|
29
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs";
|
|
30
|
+
export { Select, SelectItem, SelectGroup } from "./select";
|
|
31
|
+
export { Tooltip } from "./tooltip";
|
|
32
|
+
export { Card, CardHeader, CardBody, CardFooter } from "./card";
|
|
33
|
+
|
|
34
|
+
// ── Re-export token types for consumers ─────────────────────────────────────
|
|
35
|
+
export type { NexuiSemanticToken } from "@yugnex/nexui";
|
package/src/modal.tsx
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// @yugnex/nexui-react — Modal / Dialog
|
|
3
|
+
// Uses native <dialog> element: focus trap, Escape key, backdrop all built-in.
|
|
4
|
+
// Supports controlled (open prop) and uncontrolled usage.
|
|
5
|
+
|
|
6
|
+
import React, {
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useCallback,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
type CSSProperties,
|
|
12
|
+
} from "react";
|
|
13
|
+
|
|
14
|
+
const modalStyles: Record<string, CSSProperties> = {
|
|
15
|
+
backdrop: {
|
|
16
|
+
position: "fixed",
|
|
17
|
+
inset: 0,
|
|
18
|
+
background: "rgba(0,0,0,0.65)",
|
|
19
|
+
backdropFilter: "blur(4px)",
|
|
20
|
+
zIndex: 200,
|
|
21
|
+
display: "flex",
|
|
22
|
+
alignItems: "center",
|
|
23
|
+
justifyContent: "center",
|
|
24
|
+
padding: "16px",
|
|
25
|
+
animation: "nx-fade-in 150ms ease forwards",
|
|
26
|
+
},
|
|
27
|
+
dialog: {
|
|
28
|
+
position: "relative",
|
|
29
|
+
background: "var(--nx-bg-elevated, #1C2128)",
|
|
30
|
+
border: "1px solid var(--nx-border-strong, rgba(255,255,255,0.16))",
|
|
31
|
+
borderRadius: "12px",
|
|
32
|
+
boxShadow: "0 25px 50px rgba(0,0,0,0.65)",
|
|
33
|
+
color: "var(--nx-text, #E6EDF3)",
|
|
34
|
+
fontFamily: "var(--nx-font-sans, system-ui, sans-serif)",
|
|
35
|
+
maxHeight: "90vh",
|
|
36
|
+
display: "flex",
|
|
37
|
+
flexDirection: "column",
|
|
38
|
+
animation: "nx-scale-in 200ms cubic-bezier(0.34,1.56,0.64,1) forwards",
|
|
39
|
+
outline: "none",
|
|
40
|
+
padding: 0,
|
|
41
|
+
margin: 0,
|
|
42
|
+
width: "100%",
|
|
43
|
+
},
|
|
44
|
+
header: {
|
|
45
|
+
display: "flex",
|
|
46
|
+
alignItems: "center",
|
|
47
|
+
justifyContent: "space-between",
|
|
48
|
+
padding: "20px 24px 0",
|
|
49
|
+
flexShrink: 0,
|
|
50
|
+
},
|
|
51
|
+
title: {
|
|
52
|
+
fontSize: "var(--nx-fs-lg, 18px)",
|
|
53
|
+
fontWeight: 600,
|
|
54
|
+
color: "var(--nx-text, #E6EDF3)",
|
|
55
|
+
margin: 0,
|
|
56
|
+
lineHeight: 1.3,
|
|
57
|
+
},
|
|
58
|
+
closeBtn: {
|
|
59
|
+
width: 28,
|
|
60
|
+
height: 28,
|
|
61
|
+
borderRadius: 6,
|
|
62
|
+
border: "1px solid transparent",
|
|
63
|
+
background: "transparent",
|
|
64
|
+
color: "var(--nx-text-3, #6E7681)",
|
|
65
|
+
cursor: "pointer",
|
|
66
|
+
display: "flex",
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
justifyContent: "center",
|
|
69
|
+
flexShrink: 0,
|
|
70
|
+
transition: "background 150ms ease, color 150ms ease",
|
|
71
|
+
fontSize: 16,
|
|
72
|
+
lineHeight: 1,
|
|
73
|
+
},
|
|
74
|
+
body: {
|
|
75
|
+
padding: "16px 24px",
|
|
76
|
+
overflowY: "auto",
|
|
77
|
+
flex: 1,
|
|
78
|
+
},
|
|
79
|
+
footer: {
|
|
80
|
+
display: "flex",
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
justifyContent: "flex-end",
|
|
83
|
+
gap: 8,
|
|
84
|
+
padding: "0 24px 20px",
|
|
85
|
+
flexShrink: 0,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const SIZE_WIDTHS: Record<string, number> = {
|
|
90
|
+
sm: 400,
|
|
91
|
+
md: 560,
|
|
92
|
+
lg: 720,
|
|
93
|
+
xl: 900,
|
|
94
|
+
full: 9999,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
interface ModalProps {
|
|
98
|
+
open: boolean;
|
|
99
|
+
onClose: () => void;
|
|
100
|
+
title?: string;
|
|
101
|
+
children?: ReactNode;
|
|
102
|
+
footer?: ReactNode;
|
|
103
|
+
size?: "sm" | "md" | "lg" | "xl" | "full";
|
|
104
|
+
closeable?: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function Modal({ open, onClose, title, children, footer, size = "md", closeable = true }: ModalProps) {
|
|
108
|
+
const backdropRef = useRef<HTMLDivElement>(null);
|
|
109
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
110
|
+
|
|
111
|
+
// Focus the dialog when opened
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (open) {
|
|
114
|
+
requestAnimationFrame(() => dialogRef.current?.focus());
|
|
115
|
+
document.body.style.overflow = "hidden";
|
|
116
|
+
} else {
|
|
117
|
+
document.body.style.overflow = "";
|
|
118
|
+
}
|
|
119
|
+
return () => { document.body.style.overflow = ""; };
|
|
120
|
+
}, [open]);
|
|
121
|
+
|
|
122
|
+
// Escape key
|
|
123
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
124
|
+
if (e.key === "Escape" && closeable) onClose();
|
|
125
|
+
}, [closeable, onClose]);
|
|
126
|
+
|
|
127
|
+
// Click outside
|
|
128
|
+
const handleBackdropClick = useCallback((e: React.MouseEvent) => {
|
|
129
|
+
if (e.target === backdropRef.current && closeable) onClose();
|
|
130
|
+
}, [closeable, onClose]);
|
|
131
|
+
|
|
132
|
+
if (!open) return null;
|
|
133
|
+
|
|
134
|
+
const maxWidth = SIZE_WIDTHS[size] ?? SIZE_WIDTHS.md;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div
|
|
138
|
+
ref={backdropRef}
|
|
139
|
+
style={modalStyles.backdrop}
|
|
140
|
+
onClick={handleBackdropClick}
|
|
141
|
+
aria-modal="true"
|
|
142
|
+
role="dialog"
|
|
143
|
+
aria-label={title}
|
|
144
|
+
>
|
|
145
|
+
<div
|
|
146
|
+
ref={dialogRef}
|
|
147
|
+
style={{ ...modalStyles.dialog, maxWidth: size === "full" ? "calc(100% - 32px)" : `${maxWidth}px` }}
|
|
148
|
+
tabIndex={-1}
|
|
149
|
+
onKeyDown={handleKeyDown}
|
|
150
|
+
>
|
|
151
|
+
{(title || closeable) && (
|
|
152
|
+
<div style={modalStyles.header}>
|
|
153
|
+
{title && <h2 style={modalStyles.title}>{title}</h2>}
|
|
154
|
+
{closeable && (
|
|
155
|
+
<button
|
|
156
|
+
style={modalStyles.closeBtn}
|
|
157
|
+
onClick={onClose}
|
|
158
|
+
aria-label="Close modal"
|
|
159
|
+
onMouseEnter={e => { (e.currentTarget as HTMLButtonElement).style.background = "var(--nx-bg-overlay, #21262D)"; (e.currentTarget as HTMLButtonElement).style.color = "var(--nx-text, #E6EDF3)"; }}
|
|
160
|
+
onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.background = "transparent"; (e.currentTarget as HTMLButtonElement).style.color = "var(--nx-text-3, #6E7681)"; }}
|
|
161
|
+
>
|
|
162
|
+
✕
|
|
163
|
+
</button>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
<div style={modalStyles.body}>{children}</div>
|
|
168
|
+
{footer && <div style={modalStyles.footer}>{footer}</div>}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|