feat(website): add Phokus product site with optimized media pipeline
Single-page marketing site for phokus.jezz.wtf in the website/ pnpm workspace (React 19 + Vite 7 + Tailwind v4). Sections: Hero, Local-first, Search, Explore & timeline, Curate, Cleanup, Tech facts, Download. Product-first composition with a recurring aperture motif (EdgeMark) bleeding off alternating edges. Screenshots are transcoded to AVIF/WebP at build via vite-imagetools (sharp); ~5.3MB of masters -> ~0.5MB served. Commits only the 7 in-use captures.
@@ -7,9 +7,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build:app": "tauri build",
|
"build:app": "tauri build",
|
||||||
"build:vite": "tsc && vite build",
|
"build:vite": "tsc && vite build",
|
||||||
|
"build:web": "cd website && tsc && vite build",
|
||||||
"clean:app": "cd src-tauri && cargo clean",
|
"clean:app": "cd src-tauri && cargo clean",
|
||||||
"dev:app": "tauri dev",
|
"dev:app": "tauri dev",
|
||||||
"dev:app:cpu": "tauri dev -- --no-default-features",
|
"dev:app:cpu": "tauri dev -- --no-default-features",
|
||||||
|
"dev:web": "cd website && pnpm dev",
|
||||||
"build:app:cpu": "tauri build -- --no-default-features",
|
"build:app:cpu": "tauri build -- --no-default-features",
|
||||||
"build:app:cuda": "tauri build --config src-tauri/tauri.cuda.conf.json",
|
"build:app:cuda": "tauri build --config src-tauri/tauri.cuda.conf.json",
|
||||||
"dev:vite": "vite",
|
"dev:vite": "vite",
|
||||||
|
|||||||
@@ -76,6 +76,43 @@ importers:
|
|||||||
specifier: ^7.0.4
|
specifier: ^7.0.4
|
||||||
version: 7.3.1(jiti@2.6.1)(lightningcss@1.32.0)
|
version: 7.3.1(jiti@2.6.1)(lightningcss@1.32.0)
|
||||||
|
|
||||||
|
website:
|
||||||
|
dependencies:
|
||||||
|
framer-motion:
|
||||||
|
specifier: ^12.38.0
|
||||||
|
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react:
|
||||||
|
specifier: ^19.1.0
|
||||||
|
version: 19.2.4
|
||||||
|
react-dom:
|
||||||
|
specifier: ^19.1.0
|
||||||
|
version: 19.2.4(react@19.2.4)
|
||||||
|
devDependencies:
|
||||||
|
'@tailwindcss/vite':
|
||||||
|
specifier: ^4.2.2
|
||||||
|
version: 4.2.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0))
|
||||||
|
'@types/react':
|
||||||
|
specifier: ^19.1.8
|
||||||
|
version: 19.2.14
|
||||||
|
'@types/react-dom':
|
||||||
|
specifier: ^19.1.6
|
||||||
|
version: 19.2.3(@types/react@19.2.14)
|
||||||
|
'@vitejs/plugin-react':
|
||||||
|
specifier: ^4.6.0
|
||||||
|
version: 4.7.0(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0))
|
||||||
|
tailwindcss:
|
||||||
|
specifier: ^4.2.2
|
||||||
|
version: 4.2.2
|
||||||
|
typescript:
|
||||||
|
specifier: ~5.8.3
|
||||||
|
version: 5.8.3
|
||||||
|
vite:
|
||||||
|
specifier: ^7.0.4
|
||||||
|
version: 7.3.1(jiti@2.6.1)(lightningcss@1.32.0)
|
||||||
|
vite-imagetools:
|
||||||
|
specifier: ^10.0.0
|
||||||
|
version: 10.0.0(rollup@4.60.1)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0))
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@babel/code-frame@7.29.0':
|
'@babel/code-frame@7.29.0':
|
||||||
@@ -161,6 +198,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.11.1':
|
||||||
|
resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.27.7':
|
'@esbuild/aix-ppc64@0.27.7':
|
||||||
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
|
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -317,6 +357,159 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/colour@1.1.0':
|
||||||
|
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||||
|
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||||
|
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.34.5':
|
||||||
|
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-ppc64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-riscv64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.34.5':
|
||||||
|
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.34.5':
|
||||||
|
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [wasm32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.34.5':
|
||||||
|
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
@@ -336,6 +529,15 @@ packages:
|
|||||||
'@rolldown/pluginutils@1.0.0-beta.27':
|
'@rolldown/pluginutils@1.0.0-beta.27':
|
||||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
||||||
|
|
||||||
|
'@rollup/pluginutils@5.4.0':
|
||||||
|
resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
rollup:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.60.1':
|
'@rollup/rollup-android-arm-eabi@4.60.1':
|
||||||
resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==}
|
resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
@@ -770,6 +972,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
estree-walker@2.0.2:
|
||||||
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -805,6 +1010,10 @@ packages:
|
|||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
|
imagetools-core@9.1.0:
|
||||||
|
resolution: {integrity: sha512-xQjs+2vrxLnAjCq+omuNkd5UQTld9/bP8+YT0LyYTlKfuSQtgUBvqhUwGugzSAh6sCdN+LnROMuLswn5hZ9Fhg==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
jiti@2.6.1:
|
jiti@2.6.1:
|
||||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -955,6 +1164,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
semver@7.8.4:
|
||||||
|
resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
sharp@0.34.5:
|
||||||
|
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -984,6 +1202,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
browserslist: '>= 4.21.0'
|
browserslist: '>= 4.21.0'
|
||||||
|
|
||||||
|
vite-imagetools@10.0.0:
|
||||||
|
resolution: {integrity: sha512-+83L32YPU/2BOHWhudO2+9T5HBvb3+0qHoUNN7fb0+XcAoXilx7aE25cDPWU5kBi5Yc750zYCvHxgfyR+tAuMA==}
|
||||||
|
engines: {node: '>=22.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
vite: '>=7.0.0'
|
||||||
|
|
||||||
vite@7.3.1:
|
vite@7.3.1:
|
||||||
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1159,6 +1383,11 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.11.1':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.27.7':
|
'@esbuild/aix-ppc64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1237,6 +1466,102 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.27.7':
|
'@esbuild/win32-x64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@img/colour@1.1.0': {}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-ppc64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-riscv64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.34.5':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/runtime': 1.11.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.34.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.34.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
@@ -1258,6 +1583,14 @@ snapshots:
|
|||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||||
|
|
||||||
|
'@rollup/pluginutils@5.4.0(rollup@4.60.1)':
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.8
|
||||||
|
estree-walker: 2.0.2
|
||||||
|
picomatch: 4.0.4
|
||||||
|
optionalDependencies:
|
||||||
|
rollup: 4.60.1
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.60.1':
|
'@rollup/rollup-android-arm-eabi@4.60.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1599,6 +1932,8 @@ snapshots:
|
|||||||
|
|
||||||
escalade@3.2.0: {}
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.4):
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
@@ -1619,6 +1954,8 @@ snapshots:
|
|||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
imagetools-core@9.1.0: {}
|
||||||
|
|
||||||
jiti@2.6.1: {}
|
jiti@2.6.1: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
@@ -1750,6 +2087,39 @@ snapshots:
|
|||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
|
semver@7.8.4: {}
|
||||||
|
|
||||||
|
sharp@0.34.5:
|
||||||
|
dependencies:
|
||||||
|
'@img/colour': 1.1.0
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
semver: 7.8.4
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-darwin-arm64': 0.34.5
|
||||||
|
'@img/sharp-darwin-x64': 0.34.5
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||||
|
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||||
|
'@img/sharp-linux-arm': 0.34.5
|
||||||
|
'@img/sharp-linux-arm64': 0.34.5
|
||||||
|
'@img/sharp-linux-ppc64': 0.34.5
|
||||||
|
'@img/sharp-linux-riscv64': 0.34.5
|
||||||
|
'@img/sharp-linux-s390x': 0.34.5
|
||||||
|
'@img/sharp-linux-x64': 0.34.5
|
||||||
|
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||||
|
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||||
|
'@img/sharp-wasm32': 0.34.5
|
||||||
|
'@img/sharp-win32-arm64': 0.34.5
|
||||||
|
'@img/sharp-win32-ia32': 0.34.5
|
||||||
|
'@img/sharp-win32-x64': 0.34.5
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
tailwindcss@4.2.2: {}
|
tailwindcss@4.2.2: {}
|
||||||
@@ -1771,6 +2141,15 @@ snapshots:
|
|||||||
escalade: 3.2.0
|
escalade: 3.2.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
|
vite-imagetools@10.0.0(rollup@4.60.1)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)):
|
||||||
|
dependencies:
|
||||||
|
'@rollup/pluginutils': 5.4.0(rollup@4.60.1)
|
||||||
|
imagetools-core: 9.1.0
|
||||||
|
sharp: 0.34.5
|
||||||
|
vite: 7.3.1(jiti@2.6.1)(lightningcss@1.32.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- rollup
|
||||||
|
|
||||||
vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0):
|
vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.7
|
esbuild: 0.27.7
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
|
packages:
|
||||||
|
- website
|
||||||
|
|
||||||
allowBuilds:
|
allowBuilds:
|
||||||
esbuild: true
|
esbuild: true
|
||||||
|
sharp: true
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1002 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 785 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 756 KiB |
|
After Width: | Height: | Size: 558 KiB |
|
After Width: | Height: | Size: 443 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/brand/phokus-mark.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Phokus — your local media library, made searchable</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Phokus is a local-first desktop media library for Windows. Browse, search by meaning, and curate large image and video folders with semantic search, visual discovery, and on-device AI. Nothing leaves your machine."
|
||||||
|
/>
|
||||||
|
<meta name="theme-color" content="#0a0b0d" />
|
||||||
|
|
||||||
|
<!-- Open Graph (social card image added in the metadata pass) -->
|
||||||
|
<meta property="og:title" content="Phokus — your local media library, made searchable" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="Semantic search, visual discovery, and duplicate cleanup for the image and video folders already on your computer. Local-first, Windows."
|
||||||
|
/>
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="https://phokus.jezz.wtf" />
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@phokus/website",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"framer-motion": "^12.38.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
|
"tailwindcss": "^4.2.2",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"vite": "^7.0.4",
|
||||||
|
"vite-imagetools": "^10.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256" role="img" aria-label="Phokus">
|
||||||
|
<title>Phokus</title>
|
||||||
|
<rect x="8" y="8" width="240" height="240" rx="52" fill="#0e0f14"/>
|
||||||
|
<g transform="translate(128 128)" fill="none" stroke="#e9e9ec" stroke-width="9"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle r="110"/>
|
||||||
|
<path d="M0,-46 A52,52 0 0 0 39.84,-23 A52,52 0 0 0 39.84,23 A52,52 0 0 0 0,46 A52,52 0 0 0 -39.84,23 A52,52 0 0 0 -39.84,-23 A52,52 0 0 0 0,-46 Z"/>
|
||||||
|
<g transform="rotate(0)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
|
||||||
|
<g transform="rotate(60)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
|
||||||
|
<g transform="rotate(120)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
|
||||||
|
<g transform="rotate(180)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
|
||||||
|
<g transform="rotate(240)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
|
||||||
|
<g transform="rotate(300)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,478 @@
|
|||||||
|
import { PhokusMark } from "./components/PhokusMark";
|
||||||
|
// Screenshots are 2560×1600 masters; imagetools downscales + transcodes to
|
||||||
|
// AVIF/WebP at build (see the Shot helper). `as=picture` must be the last query
|
||||||
|
// param so the `*as=picture` module declaration matches.
|
||||||
|
import heroShot from "../assets/screenshots/phokus_galleryHero.jpg?w=1600&format=avif;webp&quality=50&as=picture";
|
||||||
|
import semanticShot from "../assets/screenshots/phokus_semanticSearch.jpg?w=1600&format=avif;webp&quality=50&as=picture";
|
||||||
|
import exploreShot from "../assets/screenshots/phokus_exploreClusters.png?w=1600&format=avif;webp&quality=50&as=picture";
|
||||||
|
import timelineShot from "../assets/screenshots/phokus_timeline.jpg?w=1600&format=avif;webp&quality=50&as=picture";
|
||||||
|
import lightboxShot from "../assets/screenshots/phokus_lightboxWithTags.jpg?w=1600&format=avif;webp&quality=50&as=picture";
|
||||||
|
import videoShot from "../assets/screenshots/phokus_videoPlayer.jpg?w=1600&format=avif;webp&quality=50&as=picture";
|
||||||
|
import duplicatesShot from "../assets/screenshots/phokus_duplicates.png?w=1600&format=avif;webp&quality=50&as=picture";
|
||||||
|
|
||||||
|
const REPO_URL = "https://github.com/JezzWTF/phokus";
|
||||||
|
const DOWNLOAD_URL = "https://github.com/JezzWTF/phokus/releases/latest";
|
||||||
|
|
||||||
|
function GitHubIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
|
||||||
|
<path d="M12 .5C5.73.5.5 5.73.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.56v-2c-3.2.7-3.88-1.54-3.88-1.54-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.71.08-.71 1.16.08 1.77 1.2 1.77 1.2 1.03 1.77 2.71 1.26 3.37.96.1-.75.4-1.26.73-1.55-2.56-.29-5.25-1.28-5.25-5.7 0-1.26.45-2.29 1.19-3.1-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11 11 0 0 1 5.8 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.81 1.18 1.84 1.18 3.1 0 4.43-2.69 5.41-5.26 5.69.41.36.78 1.06.78 2.14v3.17c0 .31.21.68.8.56A11.51 11.51 0 0 0 23.5 12C23.5 5.73 18.27.5 12 .5Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders an imagetools `?as=picture` import: AVIF + WebP sources with the WebP
|
||||||
|
// fallback on the <img>. `display:contents` so the layout/rounding lives on the
|
||||||
|
// <img>, exactly as a bare <img> would. width/height come from the transcode to
|
||||||
|
// reserve space and avoid layout shift.
|
||||||
|
type PictureImport = { sources: Record<string, string>; img: { src: string; w: number; h: number } };
|
||||||
|
|
||||||
|
function Shot({
|
||||||
|
pic,
|
||||||
|
alt,
|
||||||
|
className,
|
||||||
|
ariaHidden,
|
||||||
|
}: {
|
||||||
|
pic: PictureImport;
|
||||||
|
alt: string;
|
||||||
|
className?: string;
|
||||||
|
ariaHidden?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<picture className="contents">
|
||||||
|
{Object.entries(pic.sources).map(([type, srcSet]) => (
|
||||||
|
<source key={type} type={type} srcSet={srcSet} />
|
||||||
|
))}
|
||||||
|
<img
|
||||||
|
src={pic.img.src}
|
||||||
|
width={pic.img.w}
|
||||||
|
height={pic.img.h}
|
||||||
|
alt={alt}
|
||||||
|
aria-hidden={ariaHidden}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-50 border-b border-edge bg-canvas/70 backdrop-blur-md">
|
||||||
|
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-6">
|
||||||
|
<a href="#top" className="flex items-center gap-2.5 text-ink">
|
||||||
|
<PhokusMark className="h-6 w-6 text-amber" />
|
||||||
|
<span className="font-display text-lg font-semibold tracking-tight">Phokus</span>
|
||||||
|
</a>
|
||||||
|
<nav className="hidden items-center gap-8 text-sm text-muted md:flex">
|
||||||
|
<a href="#search" className="transition-colors hover:text-ink">Search</a>
|
||||||
|
<a href="#explore" className="transition-colors hover:text-ink">Explore</a>
|
||||||
|
<a href="#privacy" className="transition-colors hover:text-ink">Privacy</a>
|
||||||
|
<a href={REPO_URL} className="transition-colors hover:text-ink">Source</a>
|
||||||
|
</nav>
|
||||||
|
<a
|
||||||
|
href={DOWNLOAD_URL}
|
||||||
|
className="rounded-lg border border-edge-strong px-4 py-2 text-sm font-medium text-ink transition-colors hover:bg-surface"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Hero() {
|
||||||
|
return (
|
||||||
|
<section id="top" className="relative overflow-hidden lg:flex lg:min-h-[86vh] lg:items-center">
|
||||||
|
{/* Product leads: the gallery dominates the right and bleeds off the frame,
|
||||||
|
cropped so the workspace appears to continue beyond the viewport. */}
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 right-0 hidden w-[62%] lg:block">
|
||||||
|
<Shot
|
||||||
|
pic={heroShot}
|
||||||
|
alt=""
|
||||||
|
ariaHidden
|
||||||
|
className="h-full w-full object-cover object-left-top"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-canvas)_0%,transparent_46%)]" />
|
||||||
|
<div className="absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-canvas to-transparent" />
|
||||||
|
</div>
|
||||||
|
<div className="pointer-events-none absolute -left-24 top-8 -z-10 h-96 w-96 rounded-full bg-amber/[0.06] blur-3xl" />
|
||||||
|
|
||||||
|
<div className="relative mx-auto w-full max-w-7xl px-6 py-20 lg:py-24">
|
||||||
|
<div className="max-w-2xl">
|
||||||
|
<p className="mb-6 inline-flex items-center gap-2 rounded-full border border-edge bg-surface/80 px-3 py-1 text-xs font-medium text-muted backdrop-blur">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-amber" />
|
||||||
|
Local-first · Windows desktop
|
||||||
|
</p>
|
||||||
|
<h1 className="font-display text-5xl font-semibold leading-[1.04] tracking-tight text-ink sm:text-6xl">
|
||||||
|
Your local media library,
|
||||||
|
<br />
|
||||||
|
made searchable.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-6 max-w-lg text-lg leading-relaxed text-muted">
|
||||||
|
Browse, understand, and curate large image and video folders — semantic search, visual
|
||||||
|
discovery, and duplicate cleanup over the files already on your computer.
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 flex flex-wrap items-center gap-3">
|
||||||
|
<a
|
||||||
|
href={DOWNLOAD_URL}
|
||||||
|
className="rounded-lg bg-amber px-5 py-2.5 text-sm font-medium text-canvas transition-colors hover:bg-amber-soft"
|
||||||
|
>
|
||||||
|
Download for Windows
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={REPO_URL}
|
||||||
|
className="inline-flex items-center gap-2 rounded-lg border border-edge-strong bg-canvas/60 px-5 py-2.5 text-sm font-medium text-ink backdrop-blur transition-colors hover:bg-surface"
|
||||||
|
>
|
||||||
|
<GitHubIcon className="h-4 w-4" />
|
||||||
|
View source
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="mt-5 text-sm text-faint">
|
||||||
|
Windows 10/11 · Open source · On-device processing · No account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile/tablet: product sits below the copy, still bleeding off the right. */}
|
||||||
|
<div className="-mr-6 mt-2 overflow-hidden rounded-l-xl border-y border-l border-edge lg:hidden">
|
||||||
|
<Shot
|
||||||
|
pic={heroShot}
|
||||||
|
alt="The Phokus gallery: a virtualized grid of landscape photos with star ratings, a folder sidebar, filter tabs, and a search bar."
|
||||||
|
className="block w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LocalFirst() {
|
||||||
|
const points = [
|
||||||
|
{
|
||||||
|
title: "Your files stay put",
|
||||||
|
body: "Phokus reads the folders you point it at. Media and everything it derives — thumbnails, embeddings, tags, ratings — never leave your machine.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "On-device intelligence",
|
||||||
|
body: "Semantic search and AI tagging run locally. The models download once on first use; after that, search works offline.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "No account, no cloud",
|
||||||
|
body: "No sign-up, no telemetry, no server. The only network traffic is the one-time model downloads and checking for updates.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<section id="privacy" className="relative overflow-hidden border-y border-edge bg-surface">
|
||||||
|
<EdgeMark side="left" />
|
||||||
|
<div className="relative mx-auto max-w-7xl px-6 py-24 sm:py-28">
|
||||||
|
<div className="max-w-2xl">
|
||||||
|
<SectionEyebrow label="Local-first" />
|
||||||
|
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
||||||
|
A library that stays on your machine.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-5 text-lg leading-relaxed text-muted">
|
||||||
|
Phokus runs entirely on your computer. Nothing is uploaded, and there's nothing to sign
|
||||||
|
into — your photos and everything Phokus learns about them stay with you.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-12 grid gap-px overflow-hidden rounded-xl border border-edge bg-edge sm:grid-cols-3">
|
||||||
|
{points.map((p) => (
|
||||||
|
<div key={p.title} className="bg-surface p-6">
|
||||||
|
<h3 className="text-base font-medium text-ink">{p.title}</h3>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-muted">{p.body}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionEyebrow({ label }: { label: string }) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.16em] text-faint">
|
||||||
|
<PhokusMark className="h-4 w-4 text-muted" dotClassName="fill-amber" />
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchSection() {
|
||||||
|
return (
|
||||||
|
<section id="search" className="relative overflow-hidden">
|
||||||
|
<EdgeMark side="right" />
|
||||||
|
<div className="relative mx-auto max-w-7xl px-6 py-24 sm:py-28">
|
||||||
|
<div className="max-w-2xl">
|
||||||
|
<SectionEyebrow label="Search by meaning" />
|
||||||
|
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
||||||
|
Find it by what it looks like, not what it's named.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-5 text-lg leading-relaxed text-muted">
|
||||||
|
Type{" "}
|
||||||
|
<code className="rounded-md border border-edge bg-surface-2 px-1.5 py-0.5 font-mono text-[0.85em] text-ink">
|
||||||
|
/s seaside
|
||||||
|
</code>{" "}
|
||||||
|
and Phokus returns coastlines, breaking waves, and shells — ranked by visual meaning with an
|
||||||
|
on-device CLIP model. No tags required, and it keeps working offline.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<figure className="relative mt-12 overflow-hidden rounded-xl border border-edge bg-surface shadow-2xl shadow-black/50">
|
||||||
|
<Shot
|
||||||
|
pic={semanticShot}
|
||||||
|
alt="Phokus showing a semantic search for 'seaside': a grid of coastal photos — beaches, breaking waves, shells, and a jellyfish — matched by visual meaning rather than filename."
|
||||||
|
className="block w-full"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurring aperture motif. One mark per section, vertically centered and bleeding
|
||||||
|
// off the LEFT or RIGHT edge, sides alternating down the page — a clean zigzag that
|
||||||
|
// never lets two marks collide at a section boundary (corner placement did).
|
||||||
|
//
|
||||||
|
// The iris is the full mark, but with blades reconstructed to meet the rim
|
||||||
|
// *tangentially* — each blade is an arc internally tangent to the rim (radius 3.93,
|
||||||
|
// center on the line out to the tip), so it kisses the circle and peels inward to
|
||||||
|
// the opening instead of crossing it. The shipped logo's blades use radius 10
|
||||||
|
// (== rim), which reads fine at icon size but looks "broken" blown up huge; this is
|
||||||
|
// the same mark, geometrically clean at any scale.
|
||||||
|
const WM_BLADE = "M0,-4.18 A3.93,3.93 0 0 1 6.43,-7.66";
|
||||||
|
|
||||||
|
function EdgeMark({ side }: { side: "left" | "right" }) {
|
||||||
|
const place = side === "left" ? "-left-[19rem]" : "-right-[19rem]";
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`pointer-events-none absolute top-1/2 h-[34rem] w-[34rem] -translate-y-1/2 text-ink opacity-[0.05] ${place}`}
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
transform="translate(12 12)"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.1"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle r="10" />
|
||||||
|
<path d="M0,-4.18 A4.73,4.73 0 0 0 3.62,-2.09 A4.73,4.73 0 0 0 3.62,2.09 A4.73,4.73 0 0 0 0,4.18 A4.73,4.73 0 0 0 -3.62,2.09 A4.73,4.73 0 0 0 -3.62,-2.09 A4.73,4.73 0 0 0 0,-4.18 Z" />
|
||||||
|
<path d={WM_BLADE} />
|
||||||
|
<path d={WM_BLADE} transform="rotate(60)" />
|
||||||
|
<path d={WM_BLADE} transform="rotate(120)" />
|
||||||
|
<path d={WM_BLADE} transform="rotate(180)" />
|
||||||
|
<path d={WM_BLADE} transform="rotate(240)" />
|
||||||
|
<path d={WM_BLADE} transform="rotate(300)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExploreSection() {
|
||||||
|
return (
|
||||||
|
<section id="explore" className="relative overflow-hidden border-t border-edge bg-surface">
|
||||||
|
<EdgeMark side="left" />
|
||||||
|
<div className="relative mx-auto max-w-7xl px-6 py-24 sm:py-28">
|
||||||
|
<div className="grid items-center gap-10 lg:grid-cols-12">
|
||||||
|
<div className="lg:col-span-5">
|
||||||
|
<SectionEyebrow label="Explore & timeline" />
|
||||||
|
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
||||||
|
See the shape of everything you've shot.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-5 text-lg leading-relaxed text-muted">
|
||||||
|
Explore groups your library into visual clusters — sets of similar shots you can open and
|
||||||
|
wander, gathered by what they look like rather than where they're filed. A tag cloud
|
||||||
|
surfaces recurring themes, and the Timeline walks everything by the date it was taken.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<figure className="overflow-hidden rounded-xl border border-edge bg-canvas shadow-2xl shadow-black/50 lg:col-span-7">
|
||||||
|
<Shot
|
||||||
|
pic={exploreShot}
|
||||||
|
alt="The Explore view: clusters of related photos scattered across a dark canvas, each labelled with a count."
|
||||||
|
className="block w-full"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<figure className="mt-6 overflow-hidden rounded-xl border border-edge bg-canvas shadow-xl shadow-black/40">
|
||||||
|
<Shot
|
||||||
|
pic={timelineShot}
|
||||||
|
alt="The Timeline view: media grouped into month sections such as May 2024, June 2024, and July 2024."
|
||||||
|
className="block max-h-80 w-full object-cover object-top"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CurateSection() {
|
||||||
|
return (
|
||||||
|
<section id="curate" className="relative overflow-hidden">
|
||||||
|
<EdgeMark side="right" />
|
||||||
|
<div className="relative mx-auto max-w-7xl px-6 py-24 sm:py-28">
|
||||||
|
<div className="grid items-center gap-10 lg:grid-cols-12">
|
||||||
|
<figure className="order-2 overflow-hidden rounded-xl border border-edge bg-surface shadow-2xl shadow-black/50 lg:order-1 lg:col-span-7">
|
||||||
|
<Shot
|
||||||
|
pic={lightboxShot}
|
||||||
|
alt="The Phokus lightbox: a waterfall photo with a details panel showing rating stars, dimensions, file size, embedding status, and AI tags."
|
||||||
|
className="block w-full"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<div className="order-1 lg:order-2 lg:col-span-5">
|
||||||
|
<SectionEyebrow label="Review & curate" />
|
||||||
|
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
||||||
|
Rate, tag, and dig in — without losing your place.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-5 text-lg leading-relaxed text-muted">
|
||||||
|
Open anything full-screen with zoom and pan, set ratings and favourites, edit tags, or
|
||||||
|
search within an image for look-alikes. Video gets a proper player too — scrubbing,
|
||||||
|
volume, speed, and fullscreen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<figure className="mt-6 overflow-hidden rounded-xl border border-edge bg-surface shadow-xl shadow-black/40">
|
||||||
|
<Shot
|
||||||
|
pic={videoShot}
|
||||||
|
alt="The Phokus video player: a video open full-screen with playback controls and a details panel."
|
||||||
|
className="block max-h-[26rem] w-full object-cover object-center"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CleanupSection() {
|
||||||
|
return (
|
||||||
|
<section id="cleanup" className="relative overflow-hidden border-t border-edge bg-surface">
|
||||||
|
<EdgeMark side="left" />
|
||||||
|
<div className="relative mx-auto max-w-7xl px-6 py-24 sm:py-28">
|
||||||
|
<div className="max-w-2xl">
|
||||||
|
<SectionEyebrow label="Clean up safely" />
|
||||||
|
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
||||||
|
Find exact duplicates and reclaim the space.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-5 text-lg leading-relaxed text-muted">
|
||||||
|
A three-pass scan — size, then a sample hash, then a full hash — groups byte-identical files
|
||||||
|
so you can review each set and bulk-delete with confidence. Nothing is removed without your
|
||||||
|
say-so.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<figure className="mt-12 overflow-hidden rounded-xl border border-edge bg-canvas shadow-2xl shadow-black/50">
|
||||||
|
<Shot
|
||||||
|
pic={duplicatesShot}
|
||||||
|
alt="The Duplicate Finder: groups of duplicate photos with per-group sizes, reclaimable space, and bulk-delete controls."
|
||||||
|
className="block w-full"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TechFactsSection() {
|
||||||
|
const facts = [
|
||||||
|
{ k: "Platform", v: "Windows 10 / 11 (x64). The installer fetches the WebView2 runtime if it's missing." },
|
||||||
|
{ k: "Media", v: "Common image formats plus video — thumbnails and metadata come from a bundled FFmpeg." },
|
||||||
|
{ k: "On-device AI", v: "CLIP embeddings power semantic search (~580 MB); an optional WD tagger (~1.3 GB) adds tags." },
|
||||||
|
{ k: "Inference", v: "CPU / DirectML in the standard build; an optional CUDA build accelerates NVIDIA GPUs." },
|
||||||
|
{ k: "Storage", v: "A local SQLite database and thumbnail cache in your app-data folder. Nothing in the cloud." },
|
||||||
|
{ k: "License & build", v: "MIT, open source. The installer is currently unsigned — see the note below." },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<section id="tech" className="relative mx-auto max-w-7xl px-6 py-24 sm:py-28">
|
||||||
|
<div className="max-w-2xl">
|
||||||
|
<SectionEyebrow label="The technical facts" />
|
||||||
|
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
||||||
|
What you're actually running.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<dl className="mt-12 grid gap-px overflow-hidden rounded-xl border border-edge bg-edge sm:grid-cols-2">
|
||||||
|
{facts.map((f) => (
|
||||||
|
<div key={f.k} className="bg-canvas p-6">
|
||||||
|
<dt className="text-sm font-medium text-ink">{f.k}</dt>
|
||||||
|
<dd className="mt-1.5 text-sm leading-relaxed text-muted">{f.v}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DownloadSection() {
|
||||||
|
return (
|
||||||
|
<section id="download" className="relative overflow-hidden border-t border-edge bg-surface">
|
||||||
|
<EdgeMark side="right" />
|
||||||
|
<div className="relative mx-auto max-w-3xl px-6 py-24 text-center sm:py-28">
|
||||||
|
<PhokusMark className="mx-auto h-12 w-12 text-ink" dotClassName="fill-amber" />
|
||||||
|
<h2 className="mt-6 font-display text-4xl font-semibold tracking-tight text-ink">
|
||||||
|
Point Phokus at a folder.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-lg leading-relaxed text-muted">
|
||||||
|
Free and open source. Your library is indexed on your machine — the download is just the app.
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
||||||
|
<a
|
||||||
|
href={DOWNLOAD_URL}
|
||||||
|
className="rounded-lg bg-amber px-6 py-3 text-sm font-medium text-canvas transition-colors hover:bg-amber-soft"
|
||||||
|
>
|
||||||
|
Download for Windows
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={REPO_URL}
|
||||||
|
className="inline-flex items-center gap-2 rounded-lg border border-edge-strong px-6 py-3 text-sm font-medium text-ink transition-colors hover:bg-surface-2"
|
||||||
|
>
|
||||||
|
<GitHubIcon className="h-4 w-4" />
|
||||||
|
View source
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="mx-auto mt-10 max-w-xl rounded-lg border border-edge bg-canvas/60 p-4 text-left text-sm leading-relaxed text-muted">
|
||||||
|
<span className="font-medium text-ink">Heads up:</span> this build isn't code-signed yet, so
|
||||||
|
Windows SmartScreen will warn that the publisher is unrecognised — choose{" "}
|
||||||
|
<span className="text-ink">More info → Run anyway</span>. An NVIDIA CUDA build is on the
|
||||||
|
releases page too.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Footer() {
|
||||||
|
return (
|
||||||
|
<footer>
|
||||||
|
<div className="mx-auto flex max-w-7xl flex-col gap-4 px-6 py-10 text-sm text-faint sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="flex items-center gap-2 text-muted">
|
||||||
|
<PhokusMark className="h-5 w-5 text-amber" />
|
||||||
|
<span className="font-display font-medium text-ink">Phokus</span>
|
||||||
|
<span className="text-faint">· local-first media library</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<a href={REPO_URL} className="transition-colors hover:text-ink">GitHub</a>
|
||||||
|
<a href="https://git.jezz.wtf/JezzWTF/phokus" className="transition-colors hover:text-ink">Gitea</a>
|
||||||
|
<a href={`${REPO_URL}/blob/main/LICENSE`} className="transition-colors hover:text-ink">MIT</a>
|
||||||
|
<span>JezzWTF</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
<Hero />
|
||||||
|
<LocalFirst />
|
||||||
|
<SearchSection />
|
||||||
|
<ExploreSection />
|
||||||
|
<CurateSection />
|
||||||
|
<CleanupSection />
|
||||||
|
<TechFactsSection />
|
||||||
|
<DownloadSection />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Phokus aperture mark — copied (not imported) from the desktop app so the
|
||||||
|
// website stays decoupled from the Tauri renderer. Inherits currentColor.
|
||||||
|
const BLADE = "M0,-4.18 A10,10 0 0 1 6.43,-7.66";
|
||||||
|
|
||||||
|
export function PhokusMark({
|
||||||
|
className,
|
||||||
|
dotClassName,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
dotClassName?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden="true">
|
||||||
|
<g
|
||||||
|
transform="translate(12 12)"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle r="10" />
|
||||||
|
<path d="M0,-4.18 A4.73,4.73 0 0 0 3.62,-2.09 A4.73,4.73 0 0 0 3.62,2.09 A4.73,4.73 0 0 0 0,4.18 A4.73,4.73 0 0 0 -3.62,2.09 A4.73,4.73 0 0 0 -3.62,-2.09 A4.73,4.73 0 0 0 0,-4.18 Z" />
|
||||||
|
<path d={BLADE} />
|
||||||
|
<path d={BLADE} transform="rotate(60)" />
|
||||||
|
<path d={BLADE} transform="rotate(120)" />
|
||||||
|
<path d={BLADE} transform="rotate(180)" />
|
||||||
|
<path d={BLADE} transform="rotate(240)" />
|
||||||
|
<path d={BLADE} transform="rotate(300)" />
|
||||||
|
</g>
|
||||||
|
{dotClassName ? <circle cx="12" cy="12" r="2.6" stroke="none" className={dotClassName} /> : null}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Typing for vite-imagetools `?as=picture` imports. The query ends in `as=picture`,
|
||||||
|
// so this wildcard matches each screenshot import. Shape mirrors imagetools' Picture.
|
||||||
|
declare module "*as=picture" {
|
||||||
|
const picture: {
|
||||||
|
sources: Record<string, string>;
|
||||||
|
img: { src: string; w: number; h: number };
|
||||||
|
};
|
||||||
|
export default picture;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { App } from "./App";
|
||||||
|
import "./styles/index.css";
|
||||||
|
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
if (!root) throw new Error("Root element #root not found");
|
||||||
|
|
||||||
|
createRoot(root).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Phokus brand tokens — near-black surfaces, hairline edges, one amber accent
|
||||||
|
(the iris focal point). Borrowed from the app, not copied literally. */
|
||||||
|
@theme {
|
||||||
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-display: "Space Grotesk", "Inter", ui-sans-serif, sans-serif;
|
||||||
|
|
||||||
|
--color-canvas: #0a0b0d;
|
||||||
|
--color-surface: #0f1116;
|
||||||
|
--color-surface-2: #161922;
|
||||||
|
--color-edge: #ffffff14;
|
||||||
|
--color-edge-strong: #ffffff2b;
|
||||||
|
--color-ink: #e8e9ec;
|
||||||
|
--color-muted: #9a9ca3;
|
||||||
|
--color-faint: #6b6e76;
|
||||||
|
--color-amber: #e6a23c;
|
||||||
|
--color-amber-soft: #f0bd6f;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--color-canvas);
|
||||||
|
color: var(--color-ink);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"useDefineForClassFields": true
|
||||||
|
},
|
||||||
|
"include": ["src", "vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { imagetools } from "vite-imagetools";
|
||||||
|
|
||||||
|
// Static product site for phokus.jezz.wtf. Isolated from the Tauri renderer —
|
||||||
|
// no imports from ../src. Built and served by Ctrl on the Oracle VM.
|
||||||
|
// Screenshots are imported with ?as=picture and transcoded to AVIF/WebP at build.
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss(), imagetools({ removeMetadata: true })],
|
||||||
|
});
|
||||||