react-magic-portal 1.1.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/.commitlintrc +5 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/cd.yml +85 -0
- package/.github/workflows/ci.yml +65 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.prettierrc +6 -0
- package/.releaserc +13 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/__tests__/eslint.config.ts +25 -0
- package/__tests__/package.json +42 -0
- package/__tests__/src/MagicPortal.test.tsx +506 -0
- package/__tests__/tsconfig.json +23 -0
- package/__tests__/vite.config.ts +11 -0
- package/eslint.config.mts +16 -0
- package/package.json +64 -0
- package/packages/component/.prettierrc +6 -0
- package/packages/component/README.md +6 -0
- package/packages/component/dist/index.d.ts +27 -0
- package/packages/component/dist/index.d.ts.map +1 -0
- package/packages/component/dist/index.js +2 -0
- package/packages/component/dist/index.js.map +1 -0
- package/packages/component/eslint.config.ts +25 -0
- package/packages/component/package.json +70 -0
- package/packages/component/src/index.ts +123 -0
- package/packages/component/tsconfig.json +27 -0
- package/packages/example/.prettierrc +6 -0
- package/packages/example/README.md +6 -0
- package/packages/example/eslint.config.ts +25 -0
- package/packages/example/index.html +13 -0
- package/packages/example/package.json +32 -0
- package/packages/example/pnpm-lock.yaml +2098 -0
- package/packages/example/public/vite.svg +1 -0
- package/packages/example/src/App.css +332 -0
- package/packages/example/src/App.tsx +82 -0
- package/packages/example/src/assets/react.svg +1 -0
- package/packages/example/src/components/portal-content.tsx +33 -0
- package/packages/example/src/index.css +68 -0
- package/packages/example/src/main.tsx +13 -0
- package/packages/example/src/vite-env.d.ts +1 -0
- package/packages/example/tsconfig.json +25 -0
- package/packages/example/vite.config.ts +7 -0
- package/pnpm-workspace.yaml +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["container"],"sources":["../src/index.ts"],"sourcesContent":["import React, { useEffect, useState, useRef, useCallback } from 'react'\nimport ReactDOM from 'react-dom'\n\nexport interface MagicPortalProps {\n anchor: string | (() => Element | null) | Element | React.RefObject<Element | null> | null\n position?: 'append' | 'prepend' | 'before' | 'after'\n children: React.ReactNode\n onMount?: (anchor: Element, container: HTMLDivElement) => void\n onUnmount?: (anchor: Element, container: HTMLDivElement) => void\n ref?: React.Ref<HTMLDivElement | null>\n key?: React.Key\n}\n\nconst MagicPortal = ({ anchor, position = 'append', children, onMount, onUnmount, ref, key }: MagicPortalProps) => {\n const [container, setContainer] = useState<HTMLDivElement | null>(null)\n const anchorRef = useRef<Element | null>(null)\n\n const updateRef = useCallback(\n (element: HTMLDivElement | null) => {\n if (ref) {\n if (typeof ref === 'function') {\n ref(element)\n } else {\n ref.current = element\n }\n }\n },\n [ref]\n )\n\n const createContainer = useCallback(\n (anchorElement: Element): HTMLDivElement | null => {\n const container = document.createElement('div')\n container.style.display = 'contents'\n\n const positionMap = {\n before: 'beforebegin',\n prepend: 'afterbegin',\n append: 'beforeend',\n after: 'afterend'\n } as const\n\n const result = anchorElement.insertAdjacentElement(positionMap[position], container)\n\n return result as HTMLDivElement | null\n },\n [position]\n )\n\n const resolveAnchor = useCallback((): Element | null => {\n if (typeof anchor === 'string') {\n return document.querySelector(anchor)\n } else if (typeof anchor === 'function') {\n return anchor()\n } else if (anchor && 'current' in anchor) {\n return anchor.current\n } else {\n return anchor\n }\n }, [anchor])\n\n const updateAnchor = useCallback(() => {\n const newAnchor = resolveAnchor()\n\n setContainer((prevContainer) => {\n prevContainer?.remove()\n anchorRef.current = newAnchor\n const newContainer = newAnchor ? createContainer(newAnchor) : null\n updateRef(newContainer)\n return newContainer\n })\n }, [resolveAnchor, createContainer, updateRef])\n\n useEffect(() => {\n updateAnchor()\n\n const observer = new MutationObserver((mutations) => {\n const shouldUpdate = mutations.some((mutation) => {\n const { addedNodes, removedNodes } = mutation\n\n // Check if current anchor is removed\n if (anchorRef.current && Array.from(removedNodes).includes(anchorRef.current)) {\n return true\n }\n\n // Only check added nodes when anchor is a string selector\n if (typeof anchor === 'string') {\n return Array.from(addedNodes).some(\n (node) => node.nodeType === Node.ELEMENT_NODE && node instanceof Element && node.matches?.(anchor)\n )\n }\n\n return false\n })\n\n if (shouldUpdate) {\n updateAnchor()\n }\n })\n\n observer.observe(document.body, {\n childList: true,\n subtree: true\n })\n\n return () => observer.disconnect()\n }, [updateAnchor, anchor])\n\n useEffect(() => {\n if (anchorRef.current && container) {\n onMount?.(anchorRef.current, container)\n return () => {\n onUnmount?.(anchorRef.current!, container)\n }\n }\n }, [container, onMount, onUnmount])\n\n return container ? ReactDOM.createPortal(children, container, key) : null\n}\n\nMagicPortal.displayName = 'MagicPortal'\n\nexport default MagicPortal\n"],"mappings":"sGAaA,MAAM,GAAe,CAAE,SAAQ,WAAW,SAAU,WAAU,UAAS,YAAW,MAAK,SAA4B,CACjH,GAAM,CAAC,EAAW,GAAgB,EAAgC,KAAK,CACjE,EAAY,EAAuB,KAAK,CAExC,EAAY,EACf,GAAmC,CAC9B,IACE,OAAO,GAAQ,WACjB,EAAI,EAAQ,CAEZ,EAAI,QAAU,IAIpB,CAAC,EAAI,CACN,CAEK,EAAkB,EACrB,GAAkD,CACjD,IAAMA,EAAY,SAAS,cAAc,MAAM,CAY/C,MAXA,GAAU,MAAM,QAAU,WASX,EAAc,sBAPT,CAClB,OAAQ,cACR,QAAS,aACT,OAAQ,YACR,MAAO,WACR,CAE8D,GAAWA,EAAU,EAItF,CAAC,EAAS,CACX,CAEK,EAAgB,MAChB,OAAO,GAAW,SACb,SAAS,cAAc,EAAO,CAC5B,OAAO,GAAW,WACpB,GAAQ,CACN,GAAU,YAAa,EACzB,EAAO,QAEP,EAER,CAAC,EAAO,CAAC,CAEN,EAAe,MAAkB,CACrC,IAAM,EAAY,GAAe,CAEjC,EAAc,GAAkB,CAC9B,GAAe,QAAQ,CACvB,EAAU,QAAU,EACpB,IAAM,EAAe,EAAY,EAAgB,EAAU,CAAG,KAE9D,OADA,EAAU,EAAa,CAChB,GACP,EACD,CAAC,EAAe,EAAiB,EAAU,CAAC,CA8C/C,OA5CA,MAAgB,CACd,GAAc,CAEd,IAAM,EAAW,IAAI,iBAAkB,GAAc,CAC9B,EAAU,KAAM,GAAa,CAChD,GAAM,CAAE,aAAY,gBAAiB,EAcrC,OAXI,EAAU,SAAW,MAAM,KAAK,EAAa,CAAC,SAAS,EAAU,QAAQ,CACpE,GAIL,OAAO,GAAW,SACb,MAAM,KAAK,EAAW,CAAC,KAC3B,GAAS,EAAK,WAAa,KAAK,cAAgB,aAAgB,SAAW,EAAK,UAAU,EAAO,CACnG,CAGI,IACP,EAGA,GAAc,EAEhB,CAOF,OALA,EAAS,QAAQ,SAAS,KAAM,CAC9B,UAAW,GACX,QAAS,GACV,CAAC,KAEW,EAAS,YAAY,EACjC,CAAC,EAAc,EAAO,CAAC,CAE1B,MAAgB,CACd,GAAI,EAAU,SAAW,EAEvB,OADA,IAAU,EAAU,QAAS,EAAU,KAC1B,CACX,IAAY,EAAU,QAAU,EAAU,GAG7C,CAAC,EAAW,EAAS,EAAU,CAAC,CAE5B,EAAY,EAAS,aAAa,EAAU,EAAW,EAAI,CAAG,MAGvE,EAAY,YAAc,cAE1B,IAAA,EAAe"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import globals from 'globals'
|
|
2
|
+
import pluginJs from '@eslint/js'
|
|
3
|
+
import { defineConfig } from 'eslint/config'
|
|
4
|
+
import tseslint from 'typescript-eslint'
|
|
5
|
+
import prettierPlugin from 'eslint-plugin-prettier/recommended'
|
|
6
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
7
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
8
|
+
|
|
9
|
+
export default defineConfig([
|
|
10
|
+
{ files: ['**/*.{js,mjs,cjs,ts}'] },
|
|
11
|
+
{
|
|
12
|
+
languageOptions: {
|
|
13
|
+
globals: { ...globals.browser, ...globals.node },
|
|
14
|
+
parserOptions: { project: './tsconfig.json', tsconfigRootDir: import.meta.dirname }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
pluginJs.configs.recommended,
|
|
18
|
+
...tseslint.configs.recommended,
|
|
19
|
+
prettierPlugin,
|
|
20
|
+
reactHooks.configs['recommended-latest'],
|
|
21
|
+
reactRefresh.configs.vite,
|
|
22
|
+
{
|
|
23
|
+
ignores: ['**/dist/*']
|
|
24
|
+
}
|
|
25
|
+
])
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-magic-portal",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React Portal with dynamic mounting support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "tsdown src/index.ts --dts --format esm --sourcemap --watch",
|
|
9
|
+
"build": "tsdown src/index.ts --dts --format esm --sourcemap --minify --clean",
|
|
10
|
+
"lint": "eslint --fix --cache",
|
|
11
|
+
"check": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"react",
|
|
15
|
+
"portal",
|
|
16
|
+
"dynamic",
|
|
17
|
+
"browser-extension",
|
|
18
|
+
"content-script",
|
|
19
|
+
"dom",
|
|
20
|
+
"mutation-observer",
|
|
21
|
+
"inject",
|
|
22
|
+
"mount",
|
|
23
|
+
"anchor",
|
|
24
|
+
"reactdom",
|
|
25
|
+
"createportal",
|
|
26
|
+
"spa",
|
|
27
|
+
"single-page-application",
|
|
28
|
+
"web-extension",
|
|
29
|
+
"chrome-extension",
|
|
30
|
+
"firefox-extension",
|
|
31
|
+
"dynamic-content",
|
|
32
|
+
"dom-manipulation",
|
|
33
|
+
"typescript"
|
|
34
|
+
],
|
|
35
|
+
"author": "molvqingtai",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/molvqingtai/react-magic-portal.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/molvqingtai/react-magic-portal/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/molvqingtai/react-magic-portal#readme",
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=18.0.0",
|
|
47
|
+
"react-dom": ">=18.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@eslint/js": "^9.36.0",
|
|
51
|
+
"@types/node": "^24.5.2",
|
|
52
|
+
"@types/react": "^19.1.13",
|
|
53
|
+
"@types/react-dom": "^19.1.9",
|
|
54
|
+
"eslint": "^9.36.0",
|
|
55
|
+
"eslint-config-prettier": "^10.1.8",
|
|
56
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
57
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
58
|
+
"eslint-plugin-react-refresh": "^0.4.21",
|
|
59
|
+
"globals": "^16.4.0",
|
|
60
|
+
"prettier": "^3.6.2",
|
|
61
|
+
"react": "^19.1.1",
|
|
62
|
+
"react-dom": "^19.1.1",
|
|
63
|
+
"tsdown": "^0.15.4",
|
|
64
|
+
"typescript": "^5.9.2",
|
|
65
|
+
"typescript-eslint": "^8.44.1"
|
|
66
|
+
},
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef, useCallback } from 'react'
|
|
2
|
+
import ReactDOM from 'react-dom'
|
|
3
|
+
|
|
4
|
+
export interface MagicPortalProps {
|
|
5
|
+
anchor: string | (() => Element | null) | Element | React.RefObject<Element | null> | null
|
|
6
|
+
position?: 'append' | 'prepend' | 'before' | 'after'
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
onMount?: (anchor: Element, container: HTMLDivElement) => void
|
|
9
|
+
onUnmount?: (anchor: Element, container: HTMLDivElement) => void
|
|
10
|
+
ref?: React.Ref<HTMLDivElement | null>
|
|
11
|
+
key?: React.Key
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const MagicPortal = ({ anchor, position = 'append', children, onMount, onUnmount, ref, key }: MagicPortalProps) => {
|
|
15
|
+
const [container, setContainer] = useState<HTMLDivElement | null>(null)
|
|
16
|
+
const anchorRef = useRef<Element | null>(null)
|
|
17
|
+
|
|
18
|
+
const updateRef = useCallback(
|
|
19
|
+
(element: HTMLDivElement | null) => {
|
|
20
|
+
if (ref) {
|
|
21
|
+
if (typeof ref === 'function') {
|
|
22
|
+
ref(element)
|
|
23
|
+
} else {
|
|
24
|
+
ref.current = element
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
[ref]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const createContainer = useCallback(
|
|
32
|
+
(anchorElement: Element): HTMLDivElement | null => {
|
|
33
|
+
const container = document.createElement('div')
|
|
34
|
+
container.style.display = 'contents'
|
|
35
|
+
|
|
36
|
+
const positionMap = {
|
|
37
|
+
before: 'beforebegin',
|
|
38
|
+
prepend: 'afterbegin',
|
|
39
|
+
append: 'beforeend',
|
|
40
|
+
after: 'afterend'
|
|
41
|
+
} as const
|
|
42
|
+
|
|
43
|
+
const result = anchorElement.insertAdjacentElement(positionMap[position], container)
|
|
44
|
+
|
|
45
|
+
return result as HTMLDivElement | null
|
|
46
|
+
},
|
|
47
|
+
[position]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const resolveAnchor = useCallback((): Element | null => {
|
|
51
|
+
if (typeof anchor === 'string') {
|
|
52
|
+
return document.querySelector(anchor)
|
|
53
|
+
} else if (typeof anchor === 'function') {
|
|
54
|
+
return anchor()
|
|
55
|
+
} else if (anchor && 'current' in anchor) {
|
|
56
|
+
return anchor.current
|
|
57
|
+
} else {
|
|
58
|
+
return anchor
|
|
59
|
+
}
|
|
60
|
+
}, [anchor])
|
|
61
|
+
|
|
62
|
+
const updateAnchor = useCallback(() => {
|
|
63
|
+
const newAnchor = resolveAnchor()
|
|
64
|
+
|
|
65
|
+
setContainer((prevContainer) => {
|
|
66
|
+
prevContainer?.remove()
|
|
67
|
+
anchorRef.current = newAnchor
|
|
68
|
+
const newContainer = newAnchor ? createContainer(newAnchor) : null
|
|
69
|
+
updateRef(newContainer)
|
|
70
|
+
return newContainer
|
|
71
|
+
})
|
|
72
|
+
}, [resolveAnchor, createContainer, updateRef])
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
updateAnchor()
|
|
76
|
+
|
|
77
|
+
const observer = new MutationObserver((mutations) => {
|
|
78
|
+
const shouldUpdate = mutations.some((mutation) => {
|
|
79
|
+
const { addedNodes, removedNodes } = mutation
|
|
80
|
+
|
|
81
|
+
// Check if current anchor is removed
|
|
82
|
+
if (anchorRef.current && Array.from(removedNodes).includes(anchorRef.current)) {
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Only check added nodes when anchor is a string selector
|
|
87
|
+
if (typeof anchor === 'string') {
|
|
88
|
+
return Array.from(addedNodes).some(
|
|
89
|
+
(node) => node.nodeType === Node.ELEMENT_NODE && node instanceof Element && node.matches?.(anchor)
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (shouldUpdate) {
|
|
97
|
+
updateAnchor()
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
observer.observe(document.body, {
|
|
102
|
+
childList: true,
|
|
103
|
+
subtree: true
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return () => observer.disconnect()
|
|
107
|
+
}, [updateAnchor, anchor])
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (anchorRef.current && container) {
|
|
111
|
+
onMount?.(anchorRef.current, container)
|
|
112
|
+
return () => {
|
|
113
|
+
onUnmount?.(anchorRef.current!, container)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}, [container, onMount, onUnmount])
|
|
117
|
+
|
|
118
|
+
return container ? ReactDOM.createPortal(children, container, key) : null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
MagicPortal.displayName = 'MagicPortal'
|
|
122
|
+
|
|
123
|
+
export default MagicPortal
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".",
|
|
4
|
+
"rootDir": ".",
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"useDefineForClassFields": true,
|
|
7
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
8
|
+
"module": "ESNext",
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Build mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": false,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"declarationMap": true,
|
|
17
|
+
"outDir": "dist",
|
|
18
|
+
"jsx": "react-jsx",
|
|
19
|
+
|
|
20
|
+
/* Linting */
|
|
21
|
+
"strict": true,
|
|
22
|
+
"noUnusedLocals": true,
|
|
23
|
+
"noUnusedParameters": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true
|
|
25
|
+
},
|
|
26
|
+
"include": ["src", "eslint.config.ts"]
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import globals from 'globals'
|
|
2
|
+
import pluginJs from '@eslint/js'
|
|
3
|
+
import { defineConfig } from 'eslint/config'
|
|
4
|
+
import tseslint from 'typescript-eslint'
|
|
5
|
+
import prettierPlugin from 'eslint-plugin-prettier/recommended'
|
|
6
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
7
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
8
|
+
|
|
9
|
+
export default defineConfig([
|
|
10
|
+
{ files: ['**/*.{js,mjs,cjs,ts}'] },
|
|
11
|
+
{
|
|
12
|
+
languageOptions: {
|
|
13
|
+
globals: { ...globals.browser, ...globals.node },
|
|
14
|
+
parserOptions: { project: './tsconfig.json', tsconfigRootDir: import.meta.dirname }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
pluginJs.configs.recommended,
|
|
18
|
+
...tseslint.configs.recommended,
|
|
19
|
+
prettierPlugin,
|
|
20
|
+
reactHooks.configs['recommended-latest'],
|
|
21
|
+
reactRefresh.configs.vite,
|
|
22
|
+
{
|
|
23
|
+
ignores: ['**/dist/*']
|
|
24
|
+
}
|
|
25
|
+
])
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Vite + React + TS</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "eslint --fix --cache",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"check": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"react": "^19.1.1",
|
|
15
|
+
"react-dom": "^19.1.1",
|
|
16
|
+
"react-magic-portal": "workspace:*"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@eslint/js": "^9.36.0",
|
|
20
|
+
"@types/react": "^19.1.13",
|
|
21
|
+
"@types/react-dom": "^19.1.9",
|
|
22
|
+
"@vitejs/plugin-react": "^5.0.3",
|
|
23
|
+
"eslint": "^9.36.0",
|
|
24
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
25
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
26
|
+
"eslint-plugin-react-refresh": "^0.4.21",
|
|
27
|
+
"globals": "^16.4.0",
|
|
28
|
+
"typescript": "~5.9.2",
|
|
29
|
+
"typescript-eslint": "^8.44.1",
|
|
30
|
+
"vite": "^7.1.7"
|
|
31
|
+
}
|
|
32
|
+
}
|