commit ab5380a399e962b5c214e8ef96571f1df6abcf11 Author: Your Name Date: Sat Mar 15 18:32:10 2025 -0400 First refactor without AI diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b69a71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Node.js dependencies +node_modules/ +npm-debug.log +yarn-error.log +package-lock.json # Optional: include if you use yarn or want to lock versions separately + +# Build output +dist/ +build/ +*.bundle.js +*.bundle.js.map + +# Logs +logs/ +*.log +*.log.* + +# Environment variables +.env +.env.local +.env.development +.env.production + +# Editor and IDE files +.vscode/ +.idea/ +*.sublime-workspace +*.sublime-project + +# OS-specific files +.DS_Store +Thumbs.db + +# TypeScript build artifacts +*.tsbuildinfo + +# Testing and coverage +coverage/ +*.test.js +*.test.ts + +# Miscellaneous +*.swp +*.swo +*.bak \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..7ec1482 --- /dev/null +++ b/manifest.json @@ -0,0 +1,15 @@ +{ + "manifest_version": 3, + "name": "Custom New Tab", + "version": "1.0.0", + "description": "Customizable new tab page with background and search", + "chrome_url_overrides": { + "newtab": "newtab.html" + }, + "permissions": [ + "storage" + ], + "icons": { + "128": "icon.png" + } + } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2d7d7a2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,624 @@ +{ + "name": "new-tab-extension", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "new-tab-extension", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "jotai": "^2.12.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "rxjs": "^7.8.2", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@types/chrome": "^0.0.269", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.0", + "@types/uuid": "^10.0.0", + "esbuild": "^0.23.1", + "typescript": "^5.6.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/chrome": { + "version": "0.0.269", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.269.tgz", + "integrity": "sha512-vF7x8YywnhXX2F06njQ/OE7a3Qeful43C5GUOsUksXWk89WoSFUU3iLeZW8lDpVO9atm8iZIEiLQTRC3H7NOXQ==", + "dev": true, + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true + }, + "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 + }, + "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" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", + "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "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/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/jotai": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.12.2.tgz", + "integrity": "sha512-oN8715y7MkjXlSrpyjlR887TOuc/NLZMs9gvgtfWH/JP47ChwO0lR2ijSwBvPMYyXRAPT+liIAhuBavluKGgtA==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6180bd2 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "new-tab-extension", + "version": "1.0.0", + "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" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@types/chrome": "^0.0.269", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.0", + "@types/uuid": "^10.0.0", + "esbuild": "^0.23.1", + "typescript": "^5.6.2" + }, + "dependencies": { + "jotai": "^2.12.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "rxjs": "^7.8.2", + "uuid": "^10.0.0" + } +} diff --git a/public/fonts/IBMPlexMono-Regular.ttf b/public/fonts/IBMPlexMono-Regular.ttf new file mode 100644 index 0000000..81ca3dc Binary files /dev/null and b/public/fonts/IBMPlexMono-Regular.ttf differ diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..7d8646d Binary files /dev/null and b/public/icon.png differ diff --git a/public/newtab.html b/public/newtab.html new file mode 100644 index 0000000..8b1c355 --- /dev/null +++ b/public/newtab.html @@ -0,0 +1,12 @@ + + + + + New Tab + + + +
+ + + \ No newline at end of file diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..1243f11 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,307 @@ +/* Define the IBM Plex Mono font */ +@font-face { + font-family: 'IBM Plex Mono'; + src: url('/fonts/IBMPlexMono-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; +} + +body { + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; +} + +/* Base styles for widget containers */ +.container { + position: absolute; + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 5px; + padding: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +/* Styles for the close button */ +.close-button { + position: absolute; + top: 5px; + right: 5px; + width: 16px; + height: 16px; + padding: 0; + border: none; + border-radius: 50%; + background: #ccc; + color: #fff; + font-size: 12px; + line-height: 16px; + text-align: center; + cursor: pointer; + transition: background 0.2s; +} + +.close-button:hover { + background: #ff4444; +} + +.close-button:active { + background: #cc0000; +} + +/* Styles for the settings button */ +.settings-button { + position: absolute; + top: 5px; + right: 25px; + width: 16px; + height: 16px; + padding: 0; + border: none; + border-radius: 50%; + background: #ccc; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.2s; +} + +.settings-button:hover { + background: #666; +} + +.settings-button:active { + background: #333; +} + +/* Ensure container contents don't overlap the buttons */ +.container > *:not(.close-button):not(.settings-button) { + margin-top: 20px; +} + +/* Styles for ClockWidget time display */ +.clock-time { + color: #ffffff; + font-size: 48px; + font-family: 'IBM Plex Mono', monospace; + text-align: center; + margin-bottom: 10px; +} + +/* 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, 0.1); + color: #fff; +} + +/* Widget settings modal */ +.widget-settings-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.widget-modal-content { + position: relative; + background: rgba(255, 255, 255, 0.9); + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + max-width: 300px; + width: 90%; + text-align: center; +} + +.widget-modal-content label { + display: block; + margin: 10px 0; + color: #333; +} + +.widget-modal-content select, +.widget-modal-content input[type="number"] { + width: 100%; + padding: 5px; + margin-top: 5px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.widget-modal-content button { + margin-top: 10px; + padding: 5px 10px; + background: #007bff; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.widget-modal-content button:hover { + background: #0056b3; +} + +.widget-modal-content .seconds-toggle, +.widget-modal-content .hours-toggle { + text-align: left; + color: #333; +} + +.widget-modal-content input[type="checkbox"] { + margin-right: 5px; + vertical-align: middle; +} + +/* 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; + transition: background 0.2s; +} + +.settings-toggle:hover { + background: rgba(255, 255, 255, 0.4); +} + +/* Main settings modal */ +.settings-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-content { + background: rgba(255, 255, 255, 0.9); + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + max-width: 400px; + width: 90%; + text-align: center; +} + +.modal-content h2 { + margin-top: 0; + color: #333; +} + +.modal-content label { + display: block; + margin: 15px 0; + 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; +} + +.widget-buttons button { + margin: 5px; + padding: 5px 10px; + background: #007bff; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.widget-buttons button:hover { + background: #0056b3; +} + +.close-modal { + padding: 5px 10px; + background: #ccc; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.close-modal:hover { + background: #999; +} + +/* Grid overlay */ +.grid-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 500; +} + +.grid-line { + position: absolute; + background: rgba(255, 255, 255, 0.3); +} + +.grid-line.vertical { + width: 1px; + height: 100%; +} + +.grid-line.horizontal { + width: 100%; + height: 1px; +} \ No newline at end of file diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx new file mode 100644 index 0000000..42ccad6 --- /dev/null +++ b/src/renderer/App.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { NewTab } from './components/NewTab'; + +export const App: React.FC = () => { + return ; +}; \ No newline at end of file diff --git a/src/renderer/components/ClockWidget.tsx b/src/renderer/components/ClockWidget.tsx new file mode 100644 index 0000000..45e1f32 --- /dev/null +++ b/src/renderer/components/ClockWidget.tsx @@ -0,0 +1,81 @@ +import React, { useState, useEffect } from 'react'; +import { ClockWidgetData, WidgetExport } from '../types'; + +interface ClockWidgetProps { + widget: ClockWidgetData; + updateWidget: (id: string, update: Partial) => void +} + +export const ClockWidgetComponent: React.FC = ({ widget, updateWidget }) => { + const [time, setTime] = useState(new Date()); + + useEffect(() => { + const timer = setInterval(() => setTime(new Date()), 1000); + return () => clearInterval(timer); + }, []); + + const formatTime = () => { + const options: Intl.DateTimeFormatOptions = { + hour: 'numeric', + minute: '2-digit', + timeZone: widget.timezone, + hour12: !widget.use24Hour + }; + if (widget.showSeconds) { + options.second = '2-digit'; + } + return new Intl.DateTimeFormat('en-US', options).format(time); + }; + + return
{formatTime()}
; +}; + +export const ClockWidgetSettings = ({ widget, updateWidget }: ClockWidgetProps) => { + const handleToggleSeconds = (e: React.ChangeEvent) => { + updateWidget(widget.id, { showSeconds: e.target.checked }); + }; + + const handleToggle24Hour = (e: React.ChangeEvent) => { + updateWidget(widget.id, { use24Hour: e.target.checked }); + }; + + const handleTimezoneChange = (e: React.ChangeEvent) => { + updateWidget(widget.id, { timezone: e.target.value }); + }; + + return ( + <> +
+ + Show seconds +
+
+ + Use 24-hour format +
+ + + ); +}; + +export const ClockWidget:WidgetExport = { + widget: ClockWidgetComponent, + settings: ClockWidgetSettings +} \ No newline at end of file diff --git a/src/renderer/components/CogwheelIcon.tsx b/src/renderer/components/CogwheelIcon.tsx new file mode 100644 index 0000000..35e4a04 --- /dev/null +++ b/src/renderer/components/CogwheelIcon.tsx @@ -0,0 +1,12 @@ +type CogwheelIconProps = { + size: number; +} + +export function CogwheelIcon({size}: CogwheelIconProps) { + return ( + + + + + ) +} \ No newline at end of file diff --git a/src/renderer/components/NewTab.tsx b/src/renderer/components/NewTab.tsx new file mode 100644 index 0000000..ba68be0 --- /dev/null +++ b/src/renderer/components/NewTab.tsx @@ -0,0 +1,73 @@ +import React, { useState, useEffect } from 'react'; +import { useAtomValue } from 'jotai' +import { WidgetFactory } from './WidgetFactory'; +import { SettingsPanel } from './SettingsPanel'; +import { loadableWidgetsState } from '../state/widget.state'; +import { showGridState } from '../state/showGrid.state'; +import { WidgetData } from '../types'; +import { loadableBackgroundImageState } from '../state/backgroundImage.state'; +import { settingsService } from '../services/settingsService'; + +const GRID_COLS = 36; +const GRID_ROWS = 18; + +export const NewTab: React.FC = () => { + const [gridSize, setGridSize] = useState({ width: window.innerWidth / GRID_COLS, height: window.innerHeight / GRID_ROWS }); + const [widgets, setWidgets] = useState([]) + const [backgroundImage, setBackgroundImage] = useState('') + + const showGrid = useAtomValue(showGridState) + const loadingWidgets = useAtomValue(loadableWidgetsState) + const loadingBackgroundImage = useAtomValue(loadableBackgroundImageState) + + useEffect(() => { + const handleResize = () => { + setGridSize({ + width: window.innerWidth / GRID_COLS, + height: window.innerHeight / GRID_ROWS + }); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }) + + useEffect(() => loadingBackgroundImage.state === 'hasData' ? setBackgroundImage(loadingBackgroundImage.data) : undefined, [loadingBackgroundImage]); + useEffect(() => loadingWidgets.state === 'hasData' ? setWidgets(loadingWidgets.data) : undefined, [loadingWidgets]); + useEffect(() => { document.body.style.backgroundImage = `url(${backgroundImage})` }, [backgroundImage]); + + const updateWidget = (id: string, updates: Partial) => { + const updatedWidgets = widgets.map(w => + w.id === id ? { ...w, ...updates } : w + ); + setWidgets(updatedWidgets); + settingsService.saveSetting('widgets', updatedWidgets) + console.log('Updated widget:', { id, updates }) + }; + + return ( + <> + + {showGrid && ( +
+ {Array.from({ length: GRID_COLS + 1 }).map((_, i) => ( +
+ ))} + {Array.from({ length: GRID_ROWS + 1 }).map((_, i) => ( +
+ ))} +
+ )} + {widgets.map(widget => ( + + ))} + + ); +}; \ No newline at end of file diff --git a/src/renderer/components/SearchWidget.tsx b/src/renderer/components/SearchWidget.tsx new file mode 100644 index 0000000..b857701 --- /dev/null +++ b/src/renderer/components/SearchWidget.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { SearchWidgetData, WidgetExport } from '../types'; + +interface SearchWidgetProps { + widget: SearchWidgetData; + updateWidget: (id: string, update: Partial) => void +} + +const searchEngines: { [key in SearchWidgetData['engine']]: string } = { + Google: 'https://www.google.com/search?q=', + Bing: 'https://www.bing.com/search?q=', + DuckDuckGo: 'https://duckduckgo.com/?q=', + Yandex: 'https://yandex.com/search/?text=' +}; + +const SearchWidgetComponent: React.FC = ({ widget }) => { + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + const query = (e.currentTarget.elements.namedItem('query') as HTMLInputElement).value; + if (query) { + setTimeout(() => { + window.open(`${searchEngines[widget.engine]}${encodeURIComponent(query)}`, '_blank') + }, 200) + } + }; + + return ( +
+ +
+ ); +}; + +const SearchWidgetSettings = ({ widget, updateWidget }: SearchWidgetProps) => { + const handleEngineChange = (e: React.ChangeEvent) => { + updateWidget(widget.id, { engine: e.target.value as SearchWidgetData['engine'] }); + }; + + return ( + + ); +}; + +export const SearchWidget:WidgetExport = { + widget: SearchWidgetComponent, + settings: SearchWidgetSettings +} \ No newline at end of file diff --git a/src/renderer/components/SettingsPanel.tsx b/src/renderer/components/SettingsPanel.tsx new file mode 100644 index 0000000..f9085a3 --- /dev/null +++ b/src/renderer/components/SettingsPanel.tsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import { CogwheelIcon } from './CogwheelIcon'; +import { useAtom } from 'jotai'; +import { draggableState } from '../state/draggable.state'; +import { showGridState } from '../state/showGrid.state'; +import { settingsService } from '../services/settingsService'; + +interface SettingsPanelProps { + backgroundImage: string, + setBackgroundImage: (url: string) => void +} + +export const SettingsPanel: React.FC = ({ backgroundImage, setBackgroundImage }) => { + //const [backgroundUrl, setBackgroundUrl] = useAtom(backgroundImageState); + const [dragEnabled, setDragEnabled] = useAtom(draggableState); + const [showGrid, setShowGrid] = useAtom(showGridState); + const [isOpen, setIsOpen] = useState(false); + + const handleSave = () => { + //settingsService.saveSetting('backgroundUrl', backgroundUrl) + settingsService.saveSetting('dragEnabled', dragEnabled) + settingsService.saveSetting('showGrid', showGrid) + + setIsOpen(false); + }; + + const addWidget = (type: string) => { + //const newWidget = createBaseWidget(type, Math.floor(gridCols / 2), Math.floor(gridRows / 2), dragEnabled); + //saveSettings({ widgets: [...settings.widgets, newWidget] }); + }; + + return ( + <> + + {isOpen && ( +
+
+

Settings

+ + + +
+ + +
+ +
+
+ )} + + ); +}; \ No newline at end of file diff --git a/src/renderer/components/Widget.tsx b/src/renderer/components/Widget.tsx new file mode 100644 index 0000000..4a0c464 --- /dev/null +++ b/src/renderer/components/Widget.tsx @@ -0,0 +1,164 @@ +import React, { useRef, useState, useEffect } from 'react'; +import { WidgetData } from '../types'; +import { CogwheelIcon } from './CogwheelIcon'; +import { useAtom, useAtomValue } from 'jotai'; +import { draggableState } from '../state/draggable.state'; + +interface WidgetProps { + widget: WidgetData; + gridSize: { width: number; height: number }; + children: React.ReactNode; + settingsContent?: React.ReactNode; + updateWidget: (id: string, data: Partial) => void +} + +export const Widget: React.FC = ({ widget, gridSize, children, settingsContent, updateWidget }) => { + const widgetRef = useRef(null); + const modalRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [widthInput, setWidthInput] = useState(widget.gridWidth); + const [heightInput, setHeightInput] = useState(widget.gridHeight); + + const isDraggable = useAtomValue(draggableState) + const snapToGrid = (pos: number, grid: number) => Math.round(pos / grid); + + const handleMouseDown = (e: React.MouseEvent) => { + if(!isDraggable || isSettingsOpen){ + return + } + + if (widgetRef.current && e.target === widgetRef.current) { + setIsDragging(true); + const rect = widgetRef.current.getBoundingClientRect(); + setDragOffset({ + x: e.clientX - rect.left, + y: e.clientY - rect.top + }); + } + }; + + const handleMouseMove = (e: MouseEvent) => { + if (isDragging && widgetRef.current) { + e.preventDefault(); + const newGridX = snapToGrid(e.clientX - dragOffset.x, gridSize.width); + const newGridY = snapToGrid(e.clientY - dragOffset.y, gridSize.height); + + widgetRef.current.style.left = `${newGridX * gridSize.width}px`; + widgetRef.current.style.top = `${newGridY * gridSize.height}px`; + } + }; + + const handleMouseUp = () => { + if (isDragging && widgetRef.current) { + setIsDragging(false); + const rect = widgetRef.current.getBoundingClientRect(); + const newGridX = snapToGrid(rect.left, gridSize.width); + const newGridY = snapToGrid(rect.top, gridSize.height); + updateWidget(widget.id, { gridX: newGridX, gridY: newGridY }); + } + }; + + const handleClickOutside = (e: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(e.target as Node)) { + setIsSettingsOpen(false); + } + }; + + const stopPropagation = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + const handleSizeChange = () => { + const newWidth = Math.max(1, widthInput); + const newHeight = Math.max(1, heightInput); + updateWidget(widget.id, { gridWidth: newWidth, gridHeight: newHeight }); + setWidthInput(newWidth); + setHeightInput(newHeight); + setIsSettingsOpen(false); + }; + + useEffect(() => { + if (isDragging) { + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + document.body.style.userSelect = 'none'; + } + return () => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + document.body.style.userSelect = ''; + }; + }, [isDragging, dragOffset, gridSize]); + + useEffect(() => { + if (isSettingsOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isSettingsOpen]); + + useEffect(() => { + if (widgetRef.current && !isDragging) { + widgetRef.current.style.left = `${widget.gridX * gridSize.width}px`; + widgetRef.current.style.top = `${widget.gridY * gridSize.height}px`; + widgetRef.current.style.width = `${(widget.gridWidth) * gridSize.width}px`; + widgetRef.current.style.height = `${(widget.gridHeight) * gridSize.height}px`; + } + }, [widget.gridX, widget.gridY, widget.gridWidth, widget.gridHeight, gridSize, isDragging]); + + const genericSettingsContent = ( + <> + + + + + ); + + return ( +
+ + + {children} + {isSettingsOpen && ( +
+
+ + {settingsContent} + {genericSettingsContent} +
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/renderer/components/WidgetFactory.tsx b/src/renderer/components/WidgetFactory.tsx new file mode 100644 index 0000000..d813bb8 --- /dev/null +++ b/src/renderer/components/WidgetFactory.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { WidgetData, SearchWidgetData, ClockWidgetData, 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) => void +} + +const WidgetMap: { [key in WidgetTypes]: WidgetExport } = { + 'clock': ClockWidget, + 'search': SearchWidget +} + +export const WidgetFactory: React.FC = ({ widget, gridSize, updateWidget }) => { + const Wdgt = WidgetMap[widget.type] + const component = + const settings = Wdgt.settings({ widget: widget, updateWidget }) + + return ( + + {component} + + ); +}; \ No newline at end of file diff --git a/src/renderer/defaults.ts b/src/renderer/defaults.ts new file mode 100644 index 0000000..88ef549 --- /dev/null +++ b/src/renderer/defaults.ts @@ -0,0 +1,31 @@ +import { GlobalSettings, SearchWidgetData, ClockWidgetData } from './types'; + +export const DEFAULT_SETTINGS: GlobalSettings = { + backgroundUrl: '', + widgets: [ + { + id: 'clock-1', + type: 'clock', + gridX: 15, + gridY: 7, + gridWidth: 6, // Clock: 6x3 + gridHeight: 3, + dragEnabled: true, + showSeconds: true, + use24Hour: false, + timezone: 'UTC' // Default timezone + } as ClockWidgetData, + { + id: 'search-1', + type: 'search', + gridX: 13, + gridY: 10, + gridWidth: 10, // Search: 10x2 + gridHeight: 2, + dragEnabled: true, + engine: 'Google' + } as SearchWidgetData + ], + dragEnabled: true, + showGrid: false +}; \ No newline at end of file diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx new file mode 100644 index 0000000..eda0818 --- /dev/null +++ b/src/renderer/index.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; + +const root = createRoot(document.getElementById('root')!); +root.render(); \ No newline at end of file diff --git a/src/renderer/services/settingsService.ts b/src/renderer/services/settingsService.ts new file mode 100644 index 0000000..5b6f6ee --- /dev/null +++ b/src/renderer/services/settingsService.ts @@ -0,0 +1,16 @@ +import { GlobalSettings, WidgetData } from '../types'; +import { DEFAULT_SETTINGS } from '../defaults'; + +export const settingsService = { + saveSetting: async (key: K, value: GlobalSettings[K]) => { + return await chrome.storage.sync.set({ + [key]: value + }) + }, + + getSetting: async (key: K): Promise => { + const loaded = await chrome.storage.sync.get([key]) + return loaded[key] as GlobalSettings[K] + }, + +}; \ No newline at end of file diff --git a/src/renderer/state/backgroundImage.state.ts b/src/renderer/state/backgroundImage.state.ts new file mode 100644 index 0000000..9d0f10a --- /dev/null +++ b/src/renderer/state/backgroundImage.state.ts @@ -0,0 +1,9 @@ +import { atomWithObservable, loadable } from "jotai/utils"; +import { settingsService } from "../services/settingsService"; +import { from } from 'rxjs'; + +const backgroundImageState = atomWithObservable(() => + from(settingsService.getSetting('backgroundUrl')) +); + +export const loadableBackgroundImageState = loadable(backgroundImageState) \ No newline at end of file diff --git a/src/renderer/state/draggable.state.ts b/src/renderer/state/draggable.state.ts new file mode 100644 index 0000000..be373d2 --- /dev/null +++ b/src/renderer/state/draggable.state.ts @@ -0,0 +1,3 @@ +import { atom } from "jotai"; + +export const draggableState = atom(false) \ No newline at end of file diff --git a/src/renderer/state/showGrid.state.ts b/src/renderer/state/showGrid.state.ts new file mode 100644 index 0000000..53bbf29 --- /dev/null +++ b/src/renderer/state/showGrid.state.ts @@ -0,0 +1,3 @@ +import { atom } from "jotai"; + +export const showGridState = atom(false) \ No newline at end of file diff --git a/src/renderer/state/widget.state.ts b/src/renderer/state/widget.state.ts new file mode 100644 index 0000000..3891a4a --- /dev/null +++ b/src/renderer/state/widget.state.ts @@ -0,0 +1,9 @@ +import { atomWithObservable, loadable } from "jotai/utils"; +import { settingsService } from "../services/settingsService"; +import { from } from 'rxjs'; + +const widgetsState = atomWithObservable(() => + from(settingsService.getSetting('widgets')) +); + +export const loadableWidgetsState = loadable(widgetsState) \ No newline at end of file diff --git a/src/renderer/types.ts b/src/renderer/types.ts new file mode 100644 index 0000000..38da01e --- /dev/null +++ b/src/renderer/types.ts @@ -0,0 +1,35 @@ +export interface WidgetData { + id: string; + type: WidgetTypes; + gridX: number; + gridY: number; + gridWidth: number; + gridHeight: number; + [key: string]: any; +} + +export interface SearchWidgetData extends WidgetData { + type: 'search'; + engine: 'Google' | 'Bing' | 'DuckDuckGo' | 'Yandex'; +} + +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[]; + dragEnabled: boolean; + showGrid: boolean; +} + +export type WidgetExport, S = (...a:any[]) =>JSX.Element > = { + widget: W, + settings: S +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..eaac3c6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "sourceMap": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "noImplicitAny": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] + } \ No newline at end of file