First refactor without AI
This commit is contained in:
+45
@@ -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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+624
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" style="height: 100%; margin: 0; padding: 0;">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>New Tab</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body style="height: 100%; margin: 0; padding: 0;">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NewTab } from './components/NewTab';
|
||||||
|
|
||||||
|
export const App: React.FC = () => {
|
||||||
|
return <NewTab />;
|
||||||
|
};
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { ClockWidgetData, WidgetExport } from '../types';
|
||||||
|
|
||||||
|
interface ClockWidgetProps {
|
||||||
|
widget: ClockWidgetData;
|
||||||
|
updateWidget: (id: string, update: Partial<ClockWidgetData>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ClockWidgetComponent: React.FC<ClockWidgetProps> = ({ 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 <div className="clock-time">{formatTime()}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClockWidgetSettings = ({ widget, updateWidget }: ClockWidgetProps) => {
|
||||||
|
const handleToggleSeconds = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
updateWidget(widget.id, { showSeconds: e.target.checked });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggle24Hour = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
updateWidget(widget.id, { use24Hour: e.target.checked });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimezoneChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
updateWidget(widget.id, { timezone: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="seconds-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={widget.showSeconds}
|
||||||
|
onChange={handleToggleSeconds}
|
||||||
|
/>
|
||||||
|
Show seconds
|
||||||
|
</div>
|
||||||
|
<div className="hours-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={widget.use24Hour}
|
||||||
|
onChange={handleToggle24Hour}
|
||||||
|
/>
|
||||||
|
Use 24-hour format
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
Timezone:
|
||||||
|
<select value={widget.timezone} onChange={handleTimezoneChange}>
|
||||||
|
{Intl.supportedValuesOf('timeZone').map(tz => (
|
||||||
|
<option key={tz} value={tz}>
|
||||||
|
{tz}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClockWidget:WidgetExport = {
|
||||||
|
widget: ClockWidgetComponent,
|
||||||
|
settings: ClockWidgetSettings
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
type CogwheelIconProps = {
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CogwheelIcon({size}: CogwheelIconProps) {
|
||||||
|
return (
|
||||||
|
<svg 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,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<WidgetData[]>([])
|
||||||
|
const [backgroundImage, setBackgroundImage] = useState<string>('')
|
||||||
|
|
||||||
|
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 = <T extends WidgetData>(id: string, updates: Partial<T>) => {
|
||||||
|
const updatedWidgets = widgets.map(w =>
|
||||||
|
w.id === id ? { ...w, ...updates } : w
|
||||||
|
);
|
||||||
|
setWidgets(updatedWidgets);
|
||||||
|
settingsService.saveSetting('widgets', updatedWidgets)
|
||||||
|
console.log('Updated widget:', { id, updates })
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingsPanel
|
||||||
|
backgroundImage={backgroundImage}
|
||||||
|
setBackgroundImage={setBackgroundImage}
|
||||||
|
/>
|
||||||
|
{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}%` }} />
|
||||||
|
))}
|
||||||
|
{Array.from({ length: GRID_ROWS + 1 }).map((_, i) => (
|
||||||
|
<div key={`h${i}`} className="grid-line horizontal" style={{ top: `${(i / GRID_ROWS) * 100}%` }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{widgets.map(widget => (
|
||||||
|
<WidgetFactory
|
||||||
|
key={widget.id}
|
||||||
|
widget={widget}
|
||||||
|
gridSize={gridSize}
|
||||||
|
updateWidget={updateWidget}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SearchWidgetData, WidgetExport } from '../types';
|
||||||
|
|
||||||
|
interface SearchWidgetProps {
|
||||||
|
widget: SearchWidgetData;
|
||||||
|
updateWidget: (id: string, update: Partial<SearchWidgetData>) => 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<SearchWidgetProps> = ({ widget }) => {
|
||||||
|
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
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 (
|
||||||
|
<form onSubmit={handleSearch}>
|
||||||
|
<input type="text" name="query" placeholder="Search..." />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SearchWidgetSettings = ({ widget, updateWidget }: SearchWidgetProps) => {
|
||||||
|
const handleEngineChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
updateWidget(widget.id, { engine: e.target.value as SearchWidgetData['engine'] });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label>
|
||||||
|
Search Engine:
|
||||||
|
<select value={widget.engine} onChange={handleEngineChange}>
|
||||||
|
{Object.keys(searchEngines).map((engine: any) =>
|
||||||
|
<option value={engine}>{engine}</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SearchWidget:WidgetExport = {
|
||||||
|
widget: SearchWidgetComponent,
|
||||||
|
settings: SearchWidgetSettings
|
||||||
|
}
|
||||||
@@ -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<SettingsPanelProps> = ({ 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 (
|
||||||
|
<>
|
||||||
|
<button className="settings-toggle" onClick={() => setIsOpen(!isOpen)}>
|
||||||
|
<CogwheelIcon size={24} />
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="settings-modal">
|
||||||
|
<div className="modal-content">
|
||||||
|
<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={dragEnabled}
|
||||||
|
onChange={(e) => setDragEnabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Show Grid:
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={showGrid}
|
||||||
|
onChange={(e) => setShowGrid(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="widget-buttons">
|
||||||
|
<button onClick={() => addWidget('clock')}>Add Clock</button>
|
||||||
|
<button onClick={() => addWidget('search')}>Add Search</button>
|
||||||
|
</div>
|
||||||
|
<button className="close-modal" onClick={handleSave}>Save & Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<WidgetData>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, settingsContent, updateWidget }) => {
|
||||||
|
const widgetRef = useRef<HTMLDivElement>(null);
|
||||||
|
const modalRef = useRef<HTMLDivElement>(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 = (
|
||||||
|
<>
|
||||||
|
<label>
|
||||||
|
Width (grid cells):
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={widthInput}
|
||||||
|
onChange={(e) => setWidthInput(parseInt(e.target.value) || 1)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Height (grid cells):
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={heightInput}
|
||||||
|
onChange={(e) => setHeightInput(parseInt(e.target.value) || 1)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button onClick={handleSizeChange}>Apply</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={widgetRef}
|
||||||
|
className="container"
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
style={{ cursor: isDraggable && !isSettingsOpen ? 'grab' : 'default' }}
|
||||||
|
>
|
||||||
|
<button className="close-button" onClick={() => undefined /*removeWidget(widget.id)*/} onMouseDown={stopPropagation}>×</button>
|
||||||
|
<button
|
||||||
|
className="settings-button"
|
||||||
|
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||||
|
onMouseDown={stopPropagation}
|
||||||
|
>
|
||||||
|
<CogwheelIcon size={24} />
|
||||||
|
</button>
|
||||||
|
{children}
|
||||||
|
{isSettingsOpen && (
|
||||||
|
<div className="widget-settings-modal" onMouseDown={stopPropagation}>
|
||||||
|
<div ref={modalRef} className="widget-modal-content">
|
||||||
|
<button className="close-button" onClick={() => setIsSettingsOpen(false)} onMouseDown={stopPropagation}>×</button>
|
||||||
|
{settingsContent}
|
||||||
|
{genericSettingsContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<WidgetData>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const WidgetMap: { [key in WidgetTypes]: WidgetExport } = {
|
||||||
|
'clock': ClockWidget,
|
||||||
|
'search': SearchWidget
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WidgetFactory: React.FC<WidgetFactoryProps> = ({ widget, gridSize, updateWidget }) => {
|
||||||
|
const Wdgt = WidgetMap[widget.type]
|
||||||
|
const component = <Wdgt.widget widget={widget} updateWidget={updateWidget}></Wdgt.widget>
|
||||||
|
const settings = Wdgt.settings({ widget: widget, updateWidget })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Widget
|
||||||
|
widget={widget}
|
||||||
|
gridSize={gridSize}
|
||||||
|
settingsContent={settings}
|
||||||
|
updateWidget={updateWidget}
|
||||||
|
>
|
||||||
|
{component}
|
||||||
|
</Widget>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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(<App />);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { GlobalSettings, WidgetData } from '../types';
|
||||||
|
import { DEFAULT_SETTINGS } from '../defaults';
|
||||||
|
|
||||||
|
export const settingsService = {
|
||||||
|
saveSetting: async <K extends keyof GlobalSettings>(key: K, value: GlobalSettings[K]) => {
|
||||||
|
return await chrome.storage.sync.set({
|
||||||
|
[key]: value
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getSetting: async <K extends keyof GlobalSettings>(key: K): Promise<GlobalSettings[K]> => {
|
||||||
|
const loaded = await chrome.storage.sync.get([key])
|
||||||
|
return loaded[key] as GlobalSettings[K]
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
export const draggableState = atom<boolean>(false)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
export const showGridState = atom<boolean>(false)
|
||||||
@@ -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)
|
||||||
@@ -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<W = React.FC<any>, S = (...a:any[]) =>JSX.Element > = {
|
||||||
|
widget: W,
|
||||||
|
settings: S
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user