This commit is contained in:
Your Name
2025-04-18 09:52:27 -04:00
parent f569bb2240
commit 066bb11814
54 changed files with 1714 additions and 469 deletions
+1
View File
@@ -7,6 +7,7 @@ package-lock.json # Optional: include if you use yarn or want to lock versions
# Build output
dist/
build/
worker/build
*.bundle.js
*.bundle.js.map
View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+7 -3
View File
@@ -1,10 +1,10 @@
{
"manifest_version": 3,
"name": "Custom New Tab",
"version": "1.0.0",
"version": "0.0.1",
"description": "Customizable new tab page with background and search",
"chrome_url_overrides": {
"newtab": "newtab.html"
"newtab": "frontend/utab.html"
},
"permissions": [
"storage"
@@ -12,5 +12,9 @@
"icons": {
"128": "icon.png"
},
"incognito": "spanning"
"incognito": "split",
"background": {
"service_worker": "worker/main.js",
"type": "module"
}
}
+904 -5
View File
@@ -9,6 +9,10 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"jotai": "^2.12.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -24,6 +28,262 @@
"typescript": "^5.6.2"
}
},
"node_modules/@babel/code-frame": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"dependencies": {
"@babel/helper-validator-identifier": "^7.25.9",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/generator": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
"integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
"dependencies": {
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
"dependencies": {
"@babel/traverse": "^7.25.9",
"@babel/types": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"dependencies": {
"@babel/types": "^7.27.0"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
"integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.27.0",
"@babel/parser": "^7.27.0",
"@babel/template": "^7.27.0",
"@babel/types": "^7.27.0",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
"integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
"dependencies": {
"@emotion/memoize": "^0.9.0"
}
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
},
"node_modules/@emotion/react": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="
},
"node_modules/@emotion/styled": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/is-prop-valid": "^1.3.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2"
},
"peerDependencies": {
"@emotion/react": "^11.0.0-rc.0",
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
@@ -408,6 +668,283 @@
"node": ">=18"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.0.2.tgz",
"integrity": "sha512-TfeFU9TgN1N06hyb/pV/63FfO34nijZRMqgHk0TJ3gkl4Fbd+wZ73+ZtOd7jag6hMmzO9HSrBc6Vdn591nhkAg==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.0.2.tgz",
"integrity": "sha512-Bo57PFLOqXOqPNrXjd8AhzH5s6TCsNUQbvnQ0VKZ8D+lIlteqKnrk/O1luMJUc/BXONK7BfIdTdc7qOnXYbMdw==",
"dependencies": {
"@babel/runtime": "^7.27.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^7.0.2",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.0.2.tgz",
"integrity": "sha512-rjJlJ13+3LdLfobRplkXbjIFEIkn6LgpetgU/Cs3Xd8qINCCQK9qXQIjjQ6P0FXFTPFzEVMj0VgBR1mN+FhOcA==",
"dependencies": {
"@babel/runtime": "^7.27.0",
"@mui/core-downloads-tracker": "^7.0.2",
"@mui/system": "^7.0.2",
"@mui/types": "^7.4.1",
"@mui/utils": "^7.0.2",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.1.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^7.0.2",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@mui/material-pigment-css": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/private-theming": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.0.2.tgz",
"integrity": "sha512-6lt8heDC9wN8YaRqEdhqnm0cFCv08AMf4IlttFvOVn7ZdKd81PNpD/rEtPGLLwQAFyyKSxBG4/2XCgpbcdNKiA==",
"dependencies": {
"@babel/runtime": "^7.27.0",
"@mui/utils": "^7.0.2",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/styled-engine": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.0.2.tgz",
"integrity": "sha512-11Bt4YdHGlh7sB8P75S9mRCUxTlgv7HGbr0UKz6m6Z9KLeiw1Bm9y/t3iqLLVMvSHYB6zL8X8X+LmfTE++gyBw==",
"dependencies": {
"@babel/runtime": "^7.27.0",
"@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/system": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.0.2.tgz",
"integrity": "sha512-yFUraAWYWuKIISPPEVPSQ1NLeqmTT4qiQ+ktmyS8LO/KwHxB+NNVOacEZaIofh5x1NxY8rzphvU5X2heRZ/RDA==",
"dependencies": {
"@babel/runtime": "^7.27.0",
"@mui/private-theming": "^7.0.2",
"@mui/styled-engine": "^7.0.2",
"@mui/types": "^7.4.1",
"@mui/utils": "^7.0.2",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/types": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.1.tgz",
"integrity": "sha512-gUL8IIAI52CRXP/MixT1tJKt3SI6tVv4U/9soFsTtAsHzaJQptZ42ffdHZV3niX1ei0aUgMvOxBBN0KYqdG39g==",
"dependencies": {
"@babel/runtime": "^7.27.0"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.0.2.tgz",
"integrity": "sha512-72gcuQjPzhj/MLmPHLCgZjy2VjOH4KniR/4qRtXTTXIEwbkgcN+Y5W/rC90rWtMmZbjt9svZev/z+QHUI4j74w==",
"dependencies": {
"@babel/runtime": "^7.27.0",
"@mui/types": "^7.4.1",
"@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@types/chrome": {
"version": "0.0.269",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.269.tgz",
@@ -439,17 +976,20 @@
"integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==",
"dev": true
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
},
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"devOptional": true
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
},
"node_modules/@types/react": {
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -464,17 +1004,107 @@
"@types/react": "^18.0.0"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/esbuild": {
"version": "0.23.1",
@@ -515,6 +1145,96 @@
"@esbuild/win32-x64": "0.23.1"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"engines": {
"node": ">=4"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/hoist-non-react-statics/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/jotai": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.12.2.tgz",
@@ -540,6 +1260,27 @@
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -551,6 +1292,80 @@
"loose-envify": "cli.js"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dependencies": {
"callsites": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"engines": {
"node": ">=8"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -574,6 +1389,58 @@
"react": "^18.3.1"
}
},
"node_modules/react-is": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg=="
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"engines": {
"node": ">=4"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
@@ -590,6 +1457,30 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -619,6 +1510,14 @@
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"engines": {
"node": ">= 6"
}
}
}
}
+10 -3
View File
@@ -4,9 +4,12 @@
"description": "A customizable new tab page Chrome extension",
"main": "index.js",
"scripts": {
"start": "esbuild src/renderer/index.tsx --bundle --outfile=dist/index.js --sourcemap --watch",
"build": "rm -rf dist && mkdir dist && cp -r public/* dist/ && cp manifest.json dist/ && esbuild src/renderer/index.tsx --bundle --outfile=dist/index.js --minify --sourcemap",
"test": "echo \"Error: no test specified\" && exit 1"
"build": "npm run clean && npm run build-extension && npm run copy-static && npm run build-frontend && npm run build-worker",
"build-extension": "mkdir dist && cp manifest.json dist/ && cp icon.png dist/",
"copy-static": "mkdir dist/frontend && cp -r static/* dist/frontend/",
"build-frontend": "esbuild src/frontend/index.tsx --bundle --outfile=dist/frontend/index.js --minify --sourcemap",
"build-worker": "esbuild src/worker/main.ts --bundle --outfile=dist/worker/main.js --minify --sourcemap",
"clean": "rm -rf ./dist"
},
"author": "",
"license": "ISC",
@@ -19,6 +22,10 @@
"typescript": "^5.6.2"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"jotai": "^2.12.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+14
View File
@@ -0,0 +1,14 @@
import React, { useEffect } from 'react';
import { NewTab } from './components/NewTab';
import { useAtom } from 'jotai';
import { backgroundImageState } from './state/backgroundImage.state';
import { IpcService } from './services/ipcService';
export const App: React.FC = () => {
const [backgroundImage] = useAtom(backgroundImageState)
useEffect(() => { document.body.style.backgroundImage = `url(${backgroundImage})` }, [backgroundImage]);
IpcService.writeFile({ path: 'testpath', fileContent: 'testcontent' }).then(console.log)
return <NewTab />;
};
+36
View File
@@ -0,0 +1,36 @@
import React, { Suspense } from 'react';
import { useAtom } from 'jotai'
import { WidgetFactory } from './WidgetFactory';
import { gridState } from '../state/grid.state';
import { widgetState } from '../state/widget.state';
import { GlobalSettingsPanel } from './global-settings/GlobalSettingsPanel';
import { isIncognitoState } from '../state/incogntio.state';
import { GridOverlay } from './grid-overlay/GridOverlay';
export const NewTab: React.FC = () => {
const [widgets] = useAtom(widgetState)
const [grid] = useAtom(gridState)
const [isIncognitoOn] = useAtom(isIncognitoState)
const widgetsToShow = widgets.filter(widget =>
widget.showIn === 'both' ||
(!isIncognitoOn && widget.showIn === 'standard') ||
(isIncognitoOn && widget.showIn === 'incognito')
)
return (
<Suspense fallback={<></>}>
<GlobalSettingsPanel />
<GridOverlay grid={grid} />
{widgetsToShow.map(widget => (
<WidgetFactory
key={widget.id}
widget={widget}
grid={grid}
/>
))}
</Suspense>
);
};
@@ -1,12 +1,11 @@
import React, { useRef, useState, useEffect, ReactElement, useCallback, startTransition } from 'react';
import { WidgetData } from '../types';
import { CogwheelIcon } from './CogwheelIcon';
import { CogwheelIcon } from './components/CogwheelIcon';
import { useAtomValue } from 'jotai';
import { editState } from '../state/edit.state';
import { CloseButton } from './Closebutton';
import { ModalOverlay } from './ModalOverlay';
import { SettingsComponent } from './SettingsComponent';
import { debounce, debounceTime } from 'rxjs';
import { CloseButton } from './components/Closebutton';
import { ModalOverlay } from './components/ModalOverlay';
import { SettingsComponent } from './components/SettingsComponent';
interface WidgetProps {
widgetData: WidgetData;
@@ -15,9 +14,11 @@ interface WidgetProps {
settingsContent: ReactElement;
updateWidget: (id: string, data: Partial<WidgetData>) => void;
removeWidget: (id: string) => void;
disableRemove?: boolean;
disableEdit?: boolean;
}
export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children, settingsContent, updateWidget, removeWidget }) => {
export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children, settingsContent, updateWidget, removeWidget, disableEdit, disableRemove }) => {
const widgetRef = useRef<HTMLDivElement>(null);
const modalRef = useRef<HTMLDivElement>(null);
@@ -61,11 +62,11 @@ export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children,
const rect = widgetRef.current.getBoundingClientRect();
const gridX = snapToGrid(rect.left, gridSize.width);
const gridY = snapToGrid(rect.top, gridSize.height);
if(gridX !== widgetData.gridX || gridY !== widgetData.gridY){
if (gridX !== widgetData.y || gridY !== widgetData.x) {
// overwrite the values so prevent a visual "bounce"
widgetData.gridX = gridX;
widgetData.gridY = gridY;
updateWidget(widgetData.id, { gridX, gridY });
widgetData.y = gridX;
widgetData.x = gridY;
updateWidget(widgetData.id, { y: gridX, x: gridY });
}
}
};
@@ -100,10 +101,10 @@ export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children,
useEffect(() => {
if (widgetRef.current && !isDragging) {
widgetRef.current.style.left = `${widgetData.gridX * gridSize.width}px`;
widgetRef.current.style.top = `${widgetData.gridY * gridSize.height}px`;
widgetRef.current.style.width = `${(widgetData.gridWidth) * gridSize.width}px`;
widgetRef.current.style.height = `${(widgetData.gridHeight) * gridSize.height}px`;
widgetRef.current.style.left = `${widgetData.y * gridSize.width}px`;
widgetRef.current.style.top = `${widgetData.x * gridSize.height}px`;
widgetRef.current.style.width = `${(widgetData.width) * gridSize.width}px`;
widgetRef.current.style.height = `${(widgetData.height) * gridSize.height}px`;
}
}, [widgetData, gridSize, isDragging]);
@@ -115,25 +116,28 @@ export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children,
<SettingsComponent
widgetData={widgetData}
updateWidget={updateWidget}
settingsOpen={[isSettingsOpen, setSettingsOpen]}
>
{settingsContent}
</SettingsComponent>
</ModalOverlay>
<div
ref={widgetRef}
className={`container ${widgetData.background ? 'aero' : ''}`}
className={`container ${widgetData.type} ${widgetData.background ? 'aero' : ''}`}
onMouseDown={handleMouseDown}
style={{ cursor: isEditable && !isSettingsOpen ? 'grab' : 'default' }}
>
{isEditable && <>
{(isEditable && !widgetData.permanent) && <>
<CloseButton onClick={() => removeWidget(widgetData.id)} />
</>}
{(isEditable) &&
<button
className="settings-button"
onClick={() => setSettingsOpen(true)}
>
<CogwheelIcon size={24} />
</button>
</>}
}
{children}
</div>
</>);
+72
View File
@@ -0,0 +1,72 @@
import React, { useCallback, useEffect, useState } from 'react';
import { GlobalSettings, WidgetData, WidgetExport, WidgetTypeValues } from '../types';
import { Widget } from './Widget';
import { SearchWidget } from './search-widget/SearchWidget';
import { ClockWidget } from './clock-widget/ClockWidget';
import { GlobalSettingsWidget } from './global-settings/GlobalSettingsWidget';
import { useAtom } from 'jotai';
import { widgetState } from '../state/widget.state';
import { settingsService } from '../services/settingsService';
export interface WidgetFactoryProps {
widget: WidgetData;
grid: GlobalSettings['grid'];
}
const WidgetMap: { [key in WidgetTypeValues]: WidgetExport } = {
'clock': ClockWidget,
'search': SearchWidget,
'globalsettings': GlobalSettingsWidget
}
export const WidgetFactory: React.FC<WidgetFactoryProps> = ({ widget, grid }) => {
const [widgets, setWidgets] = useAtom(widgetState)
const [gridSize, setGridSize] = useState({ width: window.innerWidth / grid.cols, height: window.innerHeight / grid.rows });
useEffect(() => {
const handleResize = () => {
setGridSize({
width: window.innerWidth / grid.cols,
height: window.innerHeight / grid.rows
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [window.innerWidth, window.innerHeight, grid])
const updateWidgets = useCallback(
<T extends WidgetData>(id: string, updates: Partial<T>) => {
console.log(id, updates)
const updatedWidgets = widgets.map(w =>
w.id === id ? { ...w, ...updates } : w
);
setWidgets(updatedWidgets)
},
[widgets, setWidgets, settingsService]
);
const removeWidget = useCallback(
(widgetId: string) => {
const updatedWidgets = widgets.filter(widget => widget.id !== widgetId)
setWidgets(updatedWidgets)
},
[widgets, setWidgets, settingsService]
);
const Wdgt = WidgetMap[widget.type]
const component = <Wdgt.widget widget={widget} updateWidget={updateWidgets}></Wdgt.widget>
const settings = Wdgt.settings({ widget, updateWidget: updateWidgets })
return (
<Widget
widgetData={widget}
gridSize={gridSize}
settingsContent={settings}
updateWidget={updateWidgets}
removeWidget={removeWidget}
>
{component}
</Widget>
);
};
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { ClockWidgetData, WidgetExport } from '../types';
import { ClockWidgetData, WidgetExport } from '../../types';
interface ClockWidgetProps {
widget: ClockWidgetData;
@@ -28,8 +28,8 @@ export const ClockWidgetComponent: React.FC<ClockWidgetProps> = ({ widget, updat
};
return <>
<div className="clock-time">{formatTime()}</div>
<div className="timezone-display">{widget.timezone}</div>
<div className="time">{formatTime()}</div>
<div className="timezone">{widget.timezone}</div>
</>;
};
@@ -1,10 +1,12 @@
type CogwheelIconProps = {
size: number;
onClick?: () => void;
className?: any
}
export function CogwheelIcon({size}: CogwheelIconProps) {
export function CogwheelIcon({size, className, onClick}: CogwheelIconProps) {
return (
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<svg className={className} onClick={onClick} width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l-.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l-.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v-.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l-.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
@@ -0,0 +1,69 @@
import { Dispatch, ReactElement } from "react"
import { WidgetData } from "../../types"
import { SetStateAction, useAtom } from "jotai"
import { incognitoAllowedState, isIncognitoState } from "../../state/incogntio.state"
type GenericSettingsProps = {
widgetData: WidgetData
updateWidget: (id: string, data: Partial<WidgetData>) => void
children: ReactElement | ReactElement[]
settingsOpen?: [boolean, Dispatch<SetStateAction<boolean>>]
}
export function SettingsComponent({
widgetData,
updateWidget,
children
}: GenericSettingsProps) {
const [isIncognitoAllowed] = useAtom(incognitoAllowedState)
return <>
{children}
<label>
Width (grid cells):
<input
type="number"
min="1"
value={widgetData.width}
onChange={e => updateWidget(widgetData.id, { width: parseInt(e.target.value) })}
/>
</label>
<label>
Height (grid cells):
<input
type="number"
min="1"
value={widgetData.height}
onChange={e => updateWidget(widgetData.id, { height: parseInt(e.target.value) })}
/>
</label>
<label>
Background:
<input
type="checkbox"
checked={widgetData.background}
onChange={_ => updateWidget(widgetData.id, { background: !widgetData.background })}
/>
</label>
{isIncognitoAllowed &&
<label>
Show in standard or incognito tabs?
<select
value={widgetData.showIn}
onChange={e => updateWidget(
widgetData.id,
{ showIn: e.target.value as WidgetData['showIn'] }
)}>
{(['standard', 'incognito', 'both'] as Array<WidgetData['showIn']>)
.map(showInOption => (
<option key={showInOption} value={showInOption}>
{showInOption}
</option>
))
}
</select>
</label>
}
</>
}
@@ -0,0 +1,73 @@
import { useAtom } from "jotai";
import { ModalOverlay } from "../components/ModalOverlay";
import { useState } from "react";
import { globalSettingsOpenState } from "../../state/globalSettingsOpen.state";
import { Box, Tab, Tabs, Typography } from "@mui/material";
import { UtabSettings } from "./tab-contents/UtabSettings";
import { BackgroundSettings } from "./tab-contents/BackgroundSettings";
import { SearchEngineSettings } from "./tab-contents/SearchEngineSettings";
const settingsComponents = {
'uTab': UtabSettings,
'Background': BackgroundSettings,
'Search': SearchEngineSettings
}
export function GlobalSettingsPanel() {
const [isOpen, setOpen] = useAtom(globalSettingsOpenState)
const [tab, setTab] = useState(0);
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setTab(newValue);
};
return (
<ModalOverlay visible={isOpen} setVisible={setOpen}>
<Box sx={{ flexGrow: 1, display: 'flex', height: 600 }}>
<Tabs
orientation="vertical"
variant="scrollable"
value={tab}
onChange={handleChange}
aria-label="Vertical tabs example"
sx={{ borderRight: 1, borderColor: 'divider', textAlign: 'end' }}
>
{Object.keys(settingsComponents).map(key => <Tab label={key} />)}
</Tabs>
{Object.values(settingsComponents).map(
(Component, i) =>
<TabPanel value={tab} index={i}>
<Component></Component>
</TabPanel>
)}
</Box>
</ModalOverlay>
)
}
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`vertical-tabpanel-${index}`}
aria-labelledby={`vertical-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
@@ -0,0 +1,22 @@
import React from 'react';
import { CogwheelIcon } from '../components/CogwheelIcon';
import { useAtom } from 'jotai';
import { WidgetData, WidgetExport } from '../../types';
import { globalSettingsOpenState } from '../../state/globalSettingsOpen.state';
interface GlobalSettingsProps {
addWidget: (widget: WidgetData) => void,
}
const GlobalSettingsComponent: React.FC<GlobalSettingsProps> = ({ addWidget }) => {
const [isGlobalSettingsOpen, setGlobalSettingsOpen] = useAtom(globalSettingsOpenState)
return (
<CogwheelIcon className="settings-toggle" size={24} onClick={() => setGlobalSettingsOpen(!isGlobalSettingsOpen)} />
);
};
export const GlobalSettingsWidget: WidgetExport = {
settings: () => <></>,
widget: GlobalSettingsComponent
}
@@ -0,0 +1,17 @@
import { TextField } from "@mui/material";
import { useAtom } from "jotai";
import { backgroundImageState } from "../../../state/backgroundImage.state";
export function BackgroundSettings() {
const [backgroundImage, setBackgroundImage] = useAtom(backgroundImageState)
return (<>
<TextField
type="text"
value={backgroundImage}
onChange={(e) => setBackgroundImage(e.target.value)}
placeholder="Enter image URL"
/>
</>)
}
@@ -0,0 +1,83 @@
import { Box, Button, IconButton, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
import { useAtom } from "jotai";
import { availableEnginesState } from "../../../state/searchEngines.state";
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add';
import TextField from '@mui/material/TextField';
import { useCallback, useState } from "react";
export function SearchEngineSettings() {
const [availableSearchEngines, setAvailableSearchEngines] = useAtom(availableEnginesState)
const [aliasInput, setAliasInput] = useState('')
const [searchStringInput, setSearchStringInput] = useState('')
const addEngine = useCallback(() => {
const newEngines = {
...availableSearchEngines,
[aliasInput]: searchStringInput
}
console.log(newEngines)
setAvailableSearchEngines(newEngines)
setAliasInput('')
setSearchStringInput('')
}, [availableSearchEngines, aliasInput, searchStringInput, setAvailableSearchEngines, setAliasInput, setSearchStringInput])
const removeEngine = useCallback((alias:string) => {
const engines = { ...availableSearchEngines }
delete engines[alias]
setAvailableSearchEngines(engines)
setAliasInput('')
setSearchStringInput('')
}, [availableSearchEngines, setAvailableSearchEngines])
return (
<Box>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="left" style={{ fontWeight: 'bold' }}>Alias</TableCell>
<TableCell align="left" style={{ fontWeight: 'bold' }}>Searchstring</TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(availableSearchEngines).map(([name, searchString]) => (
<TableRow
key={name}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell align="left">{name}</TableCell>
<TableCell align="left">{searchString}</TableCell>
<TableCell>
<Button onClick={_ => removeEngine(name)} variant="outlined" size="small" color="warning" startIcon={<DeleteIcon />}>
Remove
</Button>
</TableCell>
</TableRow>
))}
<TableRow>
<TableCell>
<TextField value={aliasInput} onChange={e => setAliasInput(e.target.value)} id="Alias-input" variant="standard" />
</TableCell>
<TableCell>
<TextField value={searchStringInput} onChange={e => setSearchStringInput(e.target.value)} id="Searchstring-input" variant="standard" />
</TableCell>
<TableCell>
<Button onClick={addEngine} variant="outlined" size="small" startIcon={<AddIcon />}>
Add
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Box>
)
}
@@ -0,0 +1,46 @@
import { Switch } from "@mui/material";
import { useAtom } from "jotai";
import { editState } from "../../../state/edit.state";
import { gridState } from "../../../state/grid.state";
import { widgetState } from "../../../state/widget.state";
import { WidgetTypeValues } from "../../../types";
import { intitialWidgetData } from "../../../state/defaults";
import { useCallback } from "react";
import { incognitoAllowedState } from "../../../state/incogntio.state";
export function UtabSettings() {
const [editEnabled, setEditEnabled] = useAtom(editState)
const [grid, setGrid] = useAtom(gridState)
const [widgets, setWidgets] = useAtom(widgetState)
const createWidget = useCallback(
(type: WidgetTypeValues) => {
const data = intitialWidgetData(type)
const updatedWidgets = [...widgets, data]
setWidgets(updatedWidgets)
},
[widgets, setWidgets]
);
const [isIncognitoAllowed] = useAtom(incognitoAllowedState)
return (<>
Incognito enabled: {isIncognitoAllowed?'yes':'no'}
<label>
Editmode:
<Switch
checked={editEnabled}
onChange={() => setEditEnabled(!editEnabled)}
/>
</label>
<label>
Show Grid:
<Switch
checked={grid.show}
onChange={() => setGrid({ ...grid, show: !grid.show})}
/>
</label>
<button onClick={() => createWidget('clock')}>Add Clock</button>
<button onClick={() => createWidget('search')}>Add Search</button>
</>)
}
@@ -0,0 +1,50 @@
import { GlobalSettings } from "../../types";
const Colors = {
'red': '#FF3333',
'blue': '#3333FF'
} as const
const coloredGrindlines = (i:number, max: number) => {
let color;
if(max%4 === 0 && i === max/4){
color = Colors.blue
}
if(max%2 === 0 && i === max/2){
color = Colors.red
}
if((max*3)%4 === 0 && i === max*3/4){
color = Colors.blue
}
return { background: color }
}
type GridOverlayProps = {
grid: GlobalSettings['grid']
}
export function GridOverlay({grid}:GridOverlayProps){
return (<>
{grid.show && (
<div className="grid-overlay">
{Array.from({ length: grid.cols + 1 }).map((_, i) => (
<div key={`v${i}`}
className="grid-line vertical"
style={{
left: `${(i / grid.cols) * 100}%`,
...coloredGrindlines(i, grid.cols)
}} />
))}
{Array.from({ length: grid.rows + 1 }).map((_, i) => (
<div key={`h${i}`}
className="grid-line horizontal"
style={{
top: `${(i / grid.rows) * 100}%`,
...coloredGrindlines(i, grid.rows),
}} />
))}
</div>
)}
</>)
}
@@ -1,7 +1,8 @@
import React, { useCallback, useEffect, useState } from 'react';
import { GlobalSettings, SearchWidgetData, WidgetExport } from '../types';
import { searchEngineState } from '../state/searchEngines.state';
import { GlobalSettings, SearchWidgetData, WidgetExport } from '../../types';
import { availableEnginesState } from '../../state/searchEngines.state';
import { useAtom, useAtomValue } from 'jotai';
import { TextField } from '@mui/material';
interface SearchWidgetProps {
widget: SearchWidgetData;
@@ -12,7 +13,7 @@ const makeSearchString = (searchStringTemplate: string, query: string) => search
const SearchWidgetComponent: React.FC<SearchWidgetProps> = ({ widget, updateWidget }) => {
const [availableEngines] = useAtom(searchEngineState)
const [availableEngines] = useAtom(availableEnginesState)
const handleSearch = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
@@ -21,9 +22,6 @@ const SearchWidgetComponent: React.FC<SearchWidgetProps> = ({ widget, updateWidg
if (query) {
widget.engines.forEach(engine => {
if (!(engine in availableEngines)) {
return
}
setTimeout(() => {
window.open(makeSearchString(availableEngines[engine], query), '_blank')
}, 200)
@@ -34,14 +32,19 @@ const SearchWidgetComponent: React.FC<SearchWidgetProps> = ({ widget, updateWidg
);
return (
<form onSubmit={handleSearch}>
<form onSubmit={handleSearch} style={{ width: '100%', height: '100%', fontSize: '50cqmin' }}>
<input type="text" name="query" placeholder={`Search ${widget.engines}`} autoComplete="off" />
</form>
);
};
const SearchWidgetSettings = ({ widget, updateWidget }: SearchWidgetProps) => {
const [availableEngines] = useAtom(searchEngineState)
const [availableEngines] = useAtom(availableEnginesState)
useEffect(() => {
const engines = widget.engines.filter(engine => Object.keys(availableEngines).includes(engine))
updateWidget(widget.id, { engines })
}, [availableEngines])
const handleEngineChange = useCallback(
(e: any) => {
+4
View File
@@ -0,0 +1,4 @@
import { FrontendBindings } from '../../shared/RPC'
export const IpcService = FrontendBindings
@@ -1,5 +1,5 @@
import { GlobalSettings, WidgetData } from '../types';
import { DEFAULT_SETTINGS } from '../defaults';
import { GlobalSettings } from '../types';
import { DEFAULT_SETTINGS } from '../state/defaults';
export const settingsService = {
saveSetting: async <K extends keyof GlobalSettings>(key: K, value: GlobalSettings[K]) => {
@@ -13,4 +13,7 @@ export const settingsService = {
return loaded[key] !== undefined ? loaded[key] as GlobalSettings[K] : DEFAULT_SETTINGS[key]
},
exportSettings: async (): Promise<GlobalSettings> => {
return await chrome.storage.sync.get(null) as GlobalSettings
}
};
@@ -1,3 +1,3 @@
import { globalSettingsAtom } from "./globalSettings.atom";
import { globalSettingsAtom } from "./util/globalSettings.atom";
export const backgroundImageState = globalSettingsAtom('backgroundUrl')
@@ -1,44 +1,61 @@
import { GlobalSettings, SearchWidgetData, ClockWidgetData, WidgetTypes, WidgetData } from './types';
import { GlobalSettings, WidgetTypeValues, WidgetTypeMap } from '../types';
export const intitialWidgetData = (type: WidgetTypes): WidgetData => {
export const intitialWidgetData = (type: WidgetTypeValues): WidgetTypeMap[typeof type] => {
switch (type) {
case 'clock':
return {
gridHeight: 3,
gridWidth: 6,
gridX: 15,
gridY: 6,
height: 6,
width: 12,
y: 30,
x: 12,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
showSeconds: false,
use24Hour: true,
type: 'clock',
background: true,
id: `${type}-${Date.now()}`
} as ClockWidgetData
showIn: 'both',
id: `${type}-${Date.now()}`,
}
case 'search':
return {
gridHeight: 1,
gridWidth: 10,
gridX: 13,
gridY: 6,
height: 2,
width: 20,
y: 26,
x: 12,
id: `${type}-${Date.now()}`,
type: 'search',
engines: ['Google'],
background: true,
} as SearchWidgetData
showIn: 'both',
}
case 'globalsettings':
return {
background: false,
height: 2,
width: 2,
y: 2,
x: 2,
id: 'globalsettings',
type: 'globalsettings',
permanent: true,
showIn: 'both'
}
}
}
export const DEFAULT_SETTINGS: GlobalSettings = {
grid: {
show: true,
cols: 72,
rows: 36
},
backgroundUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Cat_August_2010-4.jpg/2880px-Cat_August_2010-4.jpg',
widgets: [
intitialWidgetData('search'),
intitialWidgetData('clock')
intitialWidgetData('clock'),
intitialWidgetData('globalsettings')
],
editEnabled: true,
showGrid: true,
searchEngines: {
Google: 'https://www.google.com/search?q={s}',
Bing: 'https://www.bing.com/search?q={s}',
@@ -46,3 +63,4 @@ export const DEFAULT_SETTINGS: GlobalSettings = {
Yandex: 'https://yandex.com/search/?text={s}'
}
};
+3
View File
@@ -0,0 +1,3 @@
import { globalSettingsAtom } from "./util/globalSettings.atom";
export const editState = globalSettingsAtom("editEnabled")
@@ -0,0 +1,3 @@
import { atom } from "jotai";
export const globalSettingsOpenState = atom(false)
+3
View File
@@ -0,0 +1,3 @@
import { globalSettingsAtom } from "./util/globalSettings.atom";
export const gridState = globalSettingsAtom('grid')
+8
View File
@@ -0,0 +1,8 @@
import { atom } from "jotai";
import { asyncStorageProxyAtom } from "./util/asyncStorageProxy.atom";
export const incognitoAllowedState = asyncStorageProxyAtom(
() => chrome.extension.isAllowedIncognitoAccess()
)
export const isIncognitoState = atom(chrome.extension.inIncognitoContext)
@@ -0,0 +1,3 @@
import { globalSettingsAtom } from "./util/globalSettings.atom";
export const availableEnginesState = globalSettingsAtom('searchEngines')
@@ -0,0 +1,24 @@
import { atom } from "jotai";
export const asyncStorageProxyAtom = <T>(
getter: () => Promise<T>,
setter: (value: T) => Promise<void> = async () => { throw new Error('AsyncProxyAtom setter not defined') }
) => {
const storage = atom<T | undefined>(undefined)
return atom(
async (get) => {
const existing = get(storage)
if (existing === undefined) {
const remoteValue = await getter()
return remoteValue
}
return existing
},
async (get, set, update: T) => {
await setter(update)
set(storage, update)
}
);
}
@@ -0,0 +1,12 @@
import { settingsService } from "../../services/settingsService";
import { GlobalSettings } from "../../types";
import { asyncStorageProxyAtom } from "./asyncStorageProxy.atom";
export const globalSettingsAtom = <T extends keyof GlobalSettings>(key:T) =>
asyncStorageProxyAtom(
async () => {const value = await settingsService.getSetting(key); console.log('reading', key, value); return value;},
(update) => {console.log('saving', key, update); return settingsService.saveSetting(key, update)}
)
+3
View File
@@ -0,0 +1,3 @@
import { globalSettingsAtom } from "./util/globalSettings.atom";
export const widgetState = globalSettingsAtom('widgets')
+58
View File
@@ -0,0 +1,58 @@
export type WidgetData<T extends WidgetTypeValues = WidgetTypeValues> = {
id: string;
type: T;
y: number;
x: number;
width: number;
height: number;
background: boolean;
permanent?: true;
showIn: 'standard' | 'incognito' | 'both'
[key: string]: any;
}
export type SearchWidgetData = WidgetData<'search'> & {
engines: Array<string>;
}
export type ClockWidgetData = WidgetData<'clock'> & {
showSeconds: boolean;
use24Hour: boolean;
timezone: string;
}
export type GlobalSettingsData = WidgetData<'globalsettings'> & {
permanent: true;
showIn: 'both'
}
export type WidgetTypeValues = (
SearchWidgetData |
ClockWidgetData |
GlobalSettingsData
)['type']
export type WidgetTypeMap<WidgetType extends WidgetTypeValues = WidgetTypeValues> = {
[Key in WidgetType]: WidgetData<Key>
} & {
'clock': ClockWidgetData,
'search': SearchWidgetData,
'globalsettings': GlobalSettingsData,
}
export interface GlobalSettings {
grid: {
show: boolean;
rows: number;
cols: number;
}
backgroundUrl: string;
widgets: WidgetData[];
editEnabled: boolean;
searchEngines: { [key in string]: string}
}
export type WidgetExport<W = React.FC<any>, S = (...a:any[]) =>JSX.Element > = {
widget: W,
settings: S
}
-6
View File
@@ -1,6 +0,0 @@
import React from 'react';
import { NewTab } from './components/NewTab';
export const App: React.FC = () => {
return <NewTab />;
};
@@ -1,68 +0,0 @@
import React, { useCallback, useState } from 'react';
import { CogwheelIcon } from './CogwheelIcon';
import { useAtom } from 'jotai';
import { editState } from '../state/edit.state';
import { showGridState } from '../state/showGrid.state';
import { intitialWidgetData } from '../defaults';
import { WidgetData, WidgetTypes } from '../types';
import { ModalOverlay } from './ModalOverlay';
import { backgroundImageState } from '../state/backgroundImage.state';
interface SettingsPanelProps {
addWidget: (widget: WidgetData) => void,
}
export const GlobalSettingsPanel: React.FC<SettingsPanelProps> = ({
addWidget
}) => {
const [isModalShowing, setModalShowing] = useState(false);
const [editEnabled, setEditEnabled] = useAtom(editState);
const [showGrid, setShowGrid] = useAtom(showGridState);
const [backgroundImage, setBackgroundImage] = useAtom(backgroundImageState)
const createWidget = (type: WidgetTypes) => addWidget(intitialWidgetData(type));
return (
<>
<button className="settings-toggle" onClick={() => setModalShowing(true)}>
<CogwheelIcon size={24} />
</button>
<ModalOverlay
setVisible={setModalShowing}
visible={isModalShowing}
>
<h2>Settings</h2>
<label>
Background URL:
<input
type="text"
value={backgroundImage}
onChange={(e) => setBackgroundImage(e.target.value)}
placeholder="Enter image URL"
/>
</label>
<label>
Drag Enabled:
<input
type="checkbox"
checked={editEnabled}
onChange={() => setEditEnabled(!editEnabled)}
/>
</label>
<label>
Show Grid:
<input
type="checkbox"
checked={showGrid}
onChange={() => setShowGrid(!showGrid)}
/>
</label>
<div className="widget-buttons">
<button onClick={() => createWidget('clock')}>Add Clock</button>
<button onClick={() => createWidget('search')}>Add Search</button>
</div>
</ModalOverlay >
</>
);
};
-106
View File
@@ -1,106 +0,0 @@
import React, { useState, useEffect, useCallback, Suspense, startTransition } from 'react';
import { useAtom, useAtomValue } from 'jotai'
import { WidgetFactory } from './WidgetFactory';
import { GlobalSettingsPanel } from './GlobalSettingsPanel';
import { showGridState } from '../state/showGrid.state';
import { WidgetData } from '../types';
import { settingsService } from '../services/settingsService';
import { backgroundImageState } from '../state/backgroundImage.state';
import { widgetState } from '../state/widget.state';
const GRID_COLS = 36;
const GRID_ROWS = 18;
const coloredGrindlines = (i:number, max: number) => {
let color;
if(i === max/2){
color = '#FF3333'
}
return { background: color }
}
export const NewTab: React.FC = () => {
const [gridSize, setGridSize] = useState({ width: window.innerWidth / GRID_COLS, height: window.innerHeight / GRID_ROWS });
const [showGrid] = useAtom(showGridState)
const [widgets, setWidgets] = useAtom(widgetState)
const [backgroundImage] = useAtom(backgroundImageState)
useEffect(() => {
const handleResize = () => {
setGridSize({
width: window.innerWidth / GRID_COLS,
height: window.innerHeight / GRID_ROWS
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [window.innerWidth, window.innerHeight])
useEffect(() => { document.body.style.backgroundImage = `url(${backgroundImage})` }, [backgroundImage]);
const updateWidget = useCallback(
<T extends WidgetData>(id: string, updates: Partial<T>) => {
console.log(id, updates)
const updatedWidgets = widgets.map(w =>
w.id === id ? { ...w, ...updates } : w
);
setWidgets(updatedWidgets)
},
[widgets, setWidgets, settingsService]
);
const addWidget = useCallback(
(data: WidgetData) => {
const updatedWidgets = [...widgets, data]
setWidgets(updatedWidgets)
},
[widgets, setWidgets, settingsService]
);
const removeWidget = useCallback(
(widgetId: string) => {
const updatedWidgets = widgets.filter(widget => widget.id !== widgetId)
setWidgets(updatedWidgets)
},
[widgets, setWidgets, settingsService]
);
return (
<Suspense fallback={<></>}>
<GlobalSettingsPanel
addWidget={addWidget}
/>
{showGrid && (
<div className="grid-overlay">
{Array.from({ length: GRID_COLS + 1 }).map((_, i) => (
<div key={`v${i}`}
className="grid-line vertical"
style={{
left: `${(i / GRID_COLS) * 100}%`,
...coloredGrindlines(i, GRID_COLS)
}} />
))}
{Array.from({ length: GRID_ROWS + 1 }).map((_, i) => (
<div key={`h${i}`}
className="grid-line horizontal"
style={{
top: `${(i / GRID_ROWS) * 100}%`,
...coloredGrindlines(i, GRID_ROWS),
}} />
))}
</div>
)}
{widgets.map(widget => (
<WidgetFactory
key={widget.id}
widget={widget}
gridSize={gridSize}
updateWidget={updateWidget}
removeWidget={removeWidget}
/>
))}
</ Suspense>
);
};
@@ -1,44 +0,0 @@
import { ReactElement } from "react"
import { WidgetData } from "../types"
type GenericSettingsProps = {
widgetData: WidgetData
updateWidget: (id: string, data: Partial<WidgetData>) => void
children: ReactElement | ReactElement[]
}
export function SettingsComponent({
widgetData,
updateWidget,
children
}: GenericSettingsProps) {
return <>
{children}
<label>
Width (grid cells):
<input
type="number"
min="1"
value={widgetData.gridWidth}
onChange={e => updateWidget(widgetData.id, { gridWidth: parseInt(e.target.value) })}
/>
</label>
<label>
Height (grid cells):
<input
type="number"
min="1"
value={widgetData.gridHeight}
onChange={e => updateWidget(widgetData.id, { gridHeight: parseInt(e.target.value) })}
/>
</label>
<label>
Background:
<input
type="checkbox"
checked={widgetData.background}
onChange={_ => updateWidget(widgetData.id, { background: !widgetData.background })}
/>
</label>
</>
}
-35
View File
@@ -1,35 +0,0 @@
import React from 'react';
import { WidgetData, WidgetExport, WidgetTypes } from '../types';
import { Widget } from './Widget';
import { SearchWidget } from './SearchWidget';
import { ClockWidget } from './ClockWidget';
interface WidgetFactoryProps {
widget: WidgetData;
gridSize: { width: number; height: number };
updateWidget: (id: string, data: Partial<WidgetData>) => void;
removeWidget: (id:string) => void;
}
const WidgetMap: { [key in WidgetTypes]: WidgetExport } = {
'clock': ClockWidget,
'search': SearchWidget
}
export const WidgetFactory: React.FC<WidgetFactoryProps> = ({ widget, gridSize, updateWidget, removeWidget }) => {
const Wdgt = WidgetMap[widget.type]
const component = <Wdgt.widget widget={widget} updateWidget={updateWidget}></Wdgt.widget>
const settings = Wdgt.settings({ widget: widget, updateWidget })
return (
<Widget
widgetData={widget}
gridSize={gridSize}
settingsContent={settings}
updateWidget={updateWidget}
removeWidget={removeWidget}
>
{component}
</Widget>
);
};
-3
View File
@@ -1,3 +0,0 @@
import { globalSettingsAtom } from "./globalSettings.atom";
export const editState = globalSettingsAtom("editEnabled")
-28
View File
@@ -1,28 +0,0 @@
import { settingsService } from "../services/settingsService";
import { atom } from "jotai";
import { GlobalSettings } from "../types";
import { DEFAULT_SETTINGS } from "../defaults";
export const globalSettingsAtom = <T extends keyof GlobalSettings>(key: T) => {
const storage = atom<GlobalSettings[T] | undefined>(undefined)
return atom(
async (get) => {
const existing = get(storage)
if (existing === undefined) {
const remoteValue = await settingsService.getSetting(key)
console.log("load", key, remoteValue)
return remoteValue
}
return existing
},
async (get, set, update: GlobalSettings[T]) => {
console.log("save", key, update)
await settingsService.saveSetting(key, update)
set(storage, update)
}
);
}
@@ -1,3 +0,0 @@
import { globalSettingsAtom } from "./globalSettings.atom";
export const searchEngineState = globalSettingsAtom('searchEngines')
-3
View File
@@ -1,3 +0,0 @@
import { globalSettingsAtom } from "./globalSettings.atom";
export const showGridState = globalSettingsAtom('showGrid')
-3
View File
@@ -1,3 +0,0 @@
import { globalSettingsAtom } from "./globalSettings.atom";
export const widgetState = globalSettingsAtom('widgets')
-37
View File
@@ -1,37 +0,0 @@
export interface WidgetData {
id: string;
type: WidgetTypes;
gridX: number;
gridY: number;
gridWidth: number;
gridHeight: number;
background: boolean;
[key: string]: any;
}
export interface SearchWidgetData extends WidgetData {
type: 'search';
engines: Array<string>;
}
export interface ClockWidgetData extends WidgetData {
type: 'clock';
showSeconds: boolean;
use24Hour: boolean;
timezone: string;
}
export type WidgetTypes = (SearchWidgetData | ClockWidgetData)['type']
export interface GlobalSettings {
backgroundUrl: string;
widgets: WidgetData[];
editEnabled: boolean;
showGrid: boolean;
searchEngines: { [key in string]: string}
}
export type WidgetExport<W = React.FC<any>, S = (...a:any[]) =>JSX.Element > = {
widget: W,
settings: S
}
+31
View File
@@ -0,0 +1,31 @@
import { ExposedEndpoints } from "../worker/Endpoints"
type NamedFunction<ArgT, RetT, NameT extends string> = { name: NameT } & ((a: ArgT) => RetT)
type Promised<T> = T extends Promise<any> ? T : Promise<T>
const wrapIpc = <A, R, N extends string = any>(fn: NamedFunction<A, R, N>): NamedFunction<A, R extends Promise<any> ? R : Promise<R>, N> => {
const func = function (message: A) {
return new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
endpoint: fn.name,
args: message
},
(response) => { res(response) }
)
})
}
Object.defineProperty(fn, "name", { value: fn.name });
return func as NamedFunction<A, R extends Promise<any> ? R : Promise<R>, N>
}
export type RPCs = typeof ExposedEndpoints
export type FrontendIpcService = {
[endpoint in keyof RPCs]: NamedFunction<Parameters<RPCs[endpoint]>[0], Promised<ReturnType<RPCs[endpoint]>>, string>
}
export const FrontendBindings: FrontendIpcService = Object.entries(ExposedEndpoints)
.map(([endpoint, fn]) => ({ [endpoint]: wrapIpc(fn) }))
.reduce((prev, curr) => Object.assign(prev, curr), {}) as FrontendIpcService
+6
View File
@@ -0,0 +1,6 @@
export const ExposedEndpoints = {
writeFile: (message: { path: string, fileContent: string }) => {
console.log("writing", message.fileContent, "to", message.path)
return "OK"
}
} as const;
+9
View File
@@ -0,0 +1,9 @@
import { ExposedEndpoints } from "./Endpoints";
chrome.runtime.onMessage.addListener((param: { endpoint: keyof typeof ExposedEndpoints, args: any }, sender, sendReponse) => {
const returnValue = ExposedEndpoints[param.endpoint].apply({}, param.args)
if(returnValue !== undefined){
sendReponse(returnValue)
return true;
}
});
+9 -55
View File
@@ -1,7 +1,7 @@
/* Define the IBM Plex Mono font */
@font-face {
font-family: 'IBM Plex Mono';
src: url('/fonts/IBMPlexMono-Regular.ttf') format('truetype');
src: url('./fonts/IBMPlexMono-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
@@ -22,7 +22,6 @@ body {
z-index: 0;
container-type: size;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0);
padding: 10px;
border-radius: 5px;
}
@@ -31,7 +30,7 @@ body {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-color: rgba(255, 255, 255, 0.3);
border-color: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
@@ -79,36 +78,6 @@ body {
background: #333;
}
/* Styles for ClockWidget time display */
.clock-time {
color: #ffffff;
font-size: 48px;
font-family: 'IBM Plex Mono', monospace;
text-align: center;
font-size: 50cqmin;
}
.timezone-display{
font-size: small;
text-align: center;
color: #ffffff;
font-size: 15cqmin;
}
/* SearchWidget form */
.container form {
display: flex;
justify-content: center;
}
.container form input[type="text"] {
width: 100%;
padding: 5px;
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 4px;
background: rgba(255, 255, 255, 1);
color: #000;
}
.accept-button {
margin-top: 10px;
@@ -137,20 +106,18 @@ body {
/* Settings toggle (cogwheel for main settings) */
.settings-toggle {
position: fixed;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
left:0;
top:0;
width:100%;
height:100%;
}
.settings-toggle:hover {
background: rgba(255, 255, 255, 0.4);
}
@@ -171,8 +138,8 @@ body {
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
max-width: 400px;
width: 90%;
min-width: 400px;
max-width: 60vw;
text-align: center;
}
@@ -187,24 +154,11 @@ body {
color: #333;
}
.modal-content input[type="text"] {
width: 100%;
padding: 5px;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 4px;
}
.modal-content .slider {
display: block;
margin: 10px auto;
}
.modal-content input[type="checkbox"] {
margin-left: 10px;
vertical-align: middle;
}
.widget-buttons {
margin: 15px 0;
}
+1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<title>New Tab</title>
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="widget-styles.css">
</head>
<body style="height: 100%; margin: 0; padding: 0;">
<div id="root"></div>
+37
View File
@@ -0,0 +1,37 @@
@font-face {
font-family: 'IBM Plex Mono';
src: url('./fonts/IBMPlexMono-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
/* Styles for ClockWidget time display */
.clock>.time {
color: #ffffff;
font-size: 48px;
font-family: 'IBM Plex Mono', monospace;
text-align: center;
font-size: 50cqmin;
}
.clock>.timezone {
font-size: small;
text-align: center;
color: #ffffff;
font-size: 15cqmin;
}
/* Styles for SearchWidget */
.search form {
display: flex;
justify-content: center;
}
.search form input[type="text"] {
width: 100%;
padding: 5px;
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 4px;
background: rgba(255, 255, 255, 1);
color: #000;
}
+3 -4
View File
@@ -6,14 +6,13 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"rootDir": ".",
"sourceMap": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"noImplicitAny": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"include": ["./src/**/*"],
"exclude": ["dist", "node_modules"]
}