diff --git a/.gitignore b/.gitignore index 512aae1a3..30b82d7cc 100644 --- a/.gitignore +++ b/.gitignore @@ -33,11 +33,6 @@ fastlane/Preview.html fastlane/screenshots/ fastlane/test_output/ -# Node / Vite landing-page tooling. The site is built in CI (see pages.yml) and -# deployed straight to GitHub Pages, so the build output is not committed. -node_modules/ -dist-ssr/ -docs/ .mcp.json # Antigravity/Serena/RTK workspace metadata diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5aca646fe..941ebb343 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -55,8 +55,10 @@ tracked — edit `project.yml`, never the generated project. | `OpenSidecarMacTests` | `MacTests/` plus selected pure-logic files | Hostless unit tests (run by `.github/workflows/tests.yml`) | The `OpenSidecar*` names are inherited from OpenDisplay and are kept for -compatibility; the product name is MeowDisplay. `src/`, `public/` and -`tools/` are the project website, not app code. +compatibility; the product name is MeowDisplay. `tools/press-kit/` builds +the press kit and is not app code. The website, https://meowdisplay.app, is +maintained in [MeowDisplayWebsite](https://github.com/raiseCatError/MeowDisplayWebsite), +not in this repository. Android's Kotlin/Compose app is built separately from the Xcode targets; see [AndroidReceiver/README.md](AndroidReceiver/README.md). diff --git a/RELEASE_ASSETS.md b/RELEASE_ASSETS.md index 08d074ee9..a8b6d6b98 100644 --- a/RELEASE_ASSETS.md +++ b/RELEASE_ASSETS.md @@ -31,8 +31,9 @@ superseded by these real captures and removed from the README. Also already exist: app icons for Mac, Mac Receiver, and iOS (`Mac/Assets.xcassets`, `MacReceiver/Assets.xcassets`, `iOS/Assets.xcassets`) — complete size sets including the Debug variant; -marketing site assets in `assets/`/`public/` (`Github Readme Icon.png`, -`MeowDisplay-mark.png`, `logo.png`, `icon.png`/`icon-256.png`, `og.png`). +brand artwork in `assets/` (`Github Readme Icon.png`, `MeowDisplay-mark.png`, +`AppIcon-master.png`). Website artwork, including the social preview, lives in +[MeowDisplayWebsite](https://github.com/raiseCatError/MeowDisplayWebsite). ## Still worth capturing later (real device photos/screen recordings) diff --git a/index.html b/index.html deleted file mode 100644 index 63bcb944d..000000000 --- a/index.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - MeowDisplay — Turn your spare Apple devices into second monitors for your Mac (free & open source) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - diff --git a/package.json b/package.json deleted file mode 100644 index 65aedbddc..000000000 --- a/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "meowdisplay-site", - "private": true, - "version": "0.0.0", - "type": "module", - "description": "MeowDisplay landing page (Vite + React + motion). Builds into docs/ for GitHub Pages.", - "packageManager": "pnpm@10.33.0", - "scripts": { - "dev": "vite", - "build": "vite build && vite build --ssr src/entry-server.tsx --outDir dist-ssr --emptyOutDir && node tools/prerender.mjs", - "preview": "vite preview" - }, - "dependencies": { - "motion": "^12.42.0", - "react": "^19.2.7", - "react-dom": "^19.2.7" - }, - "devDependencies": { - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.3", - "typescript": "^6.0.3", - "vite": "^8.1.0" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index b375a2932..000000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,640 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - motion: - specifier: ^12.42.0 - version: 12.42.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: - specifier: ^19.2.7 - version: 19.2.7 - react-dom: - specifier: ^19.2.7 - version: 19.2.7(react@19.2.7) - devDependencies: - '@types/react': - specifier: ^19.2.17 - version: 19.2.17 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) - '@vitejs/plugin-react': - specifier: ^6.0.3 - version: 6.0.3(vite@8.1.0) - typescript: - specifier: ^6.0.3 - version: 6.0.3 - vite: - specifier: ^8.1.0 - version: 8.1.0 - -packages: - - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - - '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@oxc-project/types@0.137.0': - resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} - - '@rolldown/binding-android-arm64@1.1.2': - resolution: {integrity: sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.1.2': - resolution: {integrity: sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.1.2': - resolution: {integrity: sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.1.2': - resolution: {integrity: sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.1.2': - resolution: {integrity: sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.1.2': - resolution: {integrity: sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.1.2': - resolution: {integrity: sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.1.2': - resolution: {integrity: sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.1.2': - resolution: {integrity: sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.1.2': - resolution: {integrity: sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.1.2': - resolution: {integrity: sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.1.2': - resolution: {integrity: sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.1.2': - resolution: {integrity: sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.1.2': - resolution: {integrity: sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.1.2': - resolution: {integrity: sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - - '@vitejs/plugin-react@6.0.3': - resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 - babel-plugin-react-compiler: ^1.0.0 - vite: ^8.0.0 - peerDependenciesMeta: - '@rolldown/plugin-babel': - optional: true - babel-plugin-react-compiler: - optional: true - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - framer-motion@12.42.0: - resolution: {integrity: sha512-wp7EJnfWaaEScVygKv3e20udoRz+LbtxScsuTkakAxfXmt+ReC6WyPW2nINRAGvd+hG9odwcjBLyOTPjH5pBRA==} - peerDependencies: - '@emotion/is-prop-valid': '*' - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@emotion/is-prop-valid': - optional: true - react: - optional: true - react-dom: - optional: true - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} - - motion-dom@12.42.0: - resolution: {integrity: sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w==} - - motion-utils@12.39.0: - resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} - - motion@12.42.0: - resolution: {integrity: sha512-Qhwvu9sVl5/URSq5CNzwMCpSKK8Uhnrwb6VO977kZyj/wOCS7mWebJUnBoHx5cZU1Zv8a9BD5CSICWKAlrLJgA==} - peerDependencies: - '@emotion/is-prop-valid': '*' - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@emotion/is-prop-valid': - optional: true - react: - optional: true - react-dom: - optional: true - - nanoid@3.3.15: - resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} - engines: {node: ^10 || ^12 || >=14} - - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} - peerDependencies: - react: ^19.2.7 - - react@19.2.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} - engines: {node: '>=0.10.0'} - - rolldown@1.1.2: - resolution: {integrity: sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} - hasBin: true - - vite@8.1.0: - resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - -snapshots: - - '@emnapi/core@1.11.1': - dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.11.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@oxc-project/types@0.137.0': {} - - '@rolldown/binding-android-arm64@1.1.2': - optional: true - - '@rolldown/binding-darwin-arm64@1.1.2': - optional: true - - '@rolldown/binding-darwin-x64@1.1.2': - optional: true - - '@rolldown/binding-freebsd-x64@1.1.2': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.1.2': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.1.2': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.1.2': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.1.2': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.1.2': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.1.2': - optional: true - - '@rolldown/binding-linux-x64-musl@1.1.2': - optional: true - - '@rolldown/binding-openharmony-arm64@1.1.2': - optional: true - - '@rolldown/binding-wasm32-wasi@1.1.2': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.1.2': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.1.2': - optional: true - - '@rolldown/pluginutils@1.0.1': {} - - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/react-dom@19.2.3(@types/react@19.2.17)': - dependencies: - '@types/react': 19.2.17 - - '@types/react@19.2.17': - dependencies: - csstype: 3.2.3 - - '@vitejs/plugin-react@6.0.3(vite@8.1.0)': - dependencies: - '@rolldown/pluginutils': 1.0.1 - vite: 8.1.0 - - csstype@3.2.3: {} - - detect-libc@2.1.2: {} - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - framer-motion@12.42.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - motion-dom: 12.42.0 - motion-utils: 12.39.0 - tslib: 2.8.1 - optionalDependencies: - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - - fsevents@2.3.3: - optional: true - - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - - motion-dom@12.42.0: - dependencies: - motion-utils: 12.39.0 - - motion-utils@12.39.0: {} - - motion@12.42.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - framer-motion: 12.42.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - tslib: 2.8.1 - optionalDependencies: - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - - nanoid@3.3.15: {} - - picocolors@1.1.1: {} - - picomatch@4.0.4: {} - - postcss@8.5.15: - dependencies: - nanoid: 3.3.15 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - react-dom@19.2.7(react@19.2.7): - dependencies: - react: 19.2.7 - scheduler: 0.27.0 - - react@19.2.7: {} - - rolldown@1.1.2: - dependencies: - '@oxc-project/types': 0.137.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.2 - '@rolldown/binding-darwin-arm64': 1.1.2 - '@rolldown/binding-darwin-x64': 1.1.2 - '@rolldown/binding-freebsd-x64': 1.1.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.2 - '@rolldown/binding-linux-arm64-gnu': 1.1.2 - '@rolldown/binding-linux-arm64-musl': 1.1.2 - '@rolldown/binding-linux-ppc64-gnu': 1.1.2 - '@rolldown/binding-linux-s390x-gnu': 1.1.2 - '@rolldown/binding-linux-x64-gnu': 1.1.2 - '@rolldown/binding-linux-x64-musl': 1.1.2 - '@rolldown/binding-openharmony-arm64': 1.1.2 - '@rolldown/binding-wasm32-wasi': 1.1.2 - '@rolldown/binding-win32-arm64-msvc': 1.1.2 - '@rolldown/binding-win32-x64-msvc': 1.1.2 - - scheduler@0.27.0: {} - - source-map-js@1.2.1: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - tslib@2.8.1: {} - - typescript@6.0.3: {} - - vite@8.1.0: - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.1.2 - tinyglobby: 0.2.17 - optionalDependencies: - fsevents: 2.3.3 diff --git a/public/.nojekyll b/public/.nojekyll deleted file mode 100644 index e69de29bb..000000000 diff --git a/public/icon-256.png b/public/icon-256.png deleted file mode 100644 index cd808dcf9..000000000 Binary files a/public/icon-256.png and /dev/null differ diff --git a/public/icon.png b/public/icon.png deleted file mode 100644 index 9c6f9ebbd..000000000 Binary files a/public/icon.png and /dev/null differ diff --git a/public/logo.png b/public/logo.png deleted file mode 100644 index 28818d2fe..000000000 Binary files a/public/logo.png and /dev/null differ diff --git a/public/og.png b/public/og.png deleted file mode 100644 index ef76fdfbe..000000000 Binary files a/public/og.png and /dev/null differ diff --git a/public/privacy.html b/public/privacy.html deleted file mode 100644 index 51f2cbd3f..000000000 --- a/public/privacy.html +++ /dev/null @@ -1,212 +0,0 @@ - - - - - - Privacy — MeowDisplay - - - - - - - - - - -
-
-

Privacy policy

-

No accounts. No analytics. No servers.

-

Effective June 11, 2026 · applies to the MeowDisplay Mac and iOS apps and this website

-
-
- -
-
- -
-

MeowDisplay collects no data. There are no accounts, no analytics, - no telemetry, no crash reporting, no ads, and no servers — ours or anyone else's. - Your screen content travels directly from your Mac to your device over your USB - cable or your local network, and nowhere else. Because the source code is public, - every claim on this page is verifiable.

-
- -

What MeowDisplay does with your screen

-

MeowDisplay turns an iPhone or iPad into an extra display for your Mac. To do - that, the Mac app captures the contents of a virtual display, encodes it as video, - and sends it over a single direct connection to the MeowDisplay - app on your device:

- -

In both cases the connection is peer-to-peer: no relay, no cloud, no third - party. Nothing is recorded or stored — frames are decoded, displayed, and - discarded. Touch and scroll input travels the same direct path in the other - direction.

- -

What the apps store on your devices

-

Only local settings, only on your own hardware, never transmitted anywhere - except as described:

- - - - - - - - - - - -
DataWherePurpose
Preferences (quality, connection choice, display name)Mac / device, locallyRemembering your setup between launches
A random install identifier (UUID)Device, locallyLets your own Mac recognize that the same device is reachable over both - USB and WiFi. It is generated on the device, has no relation to your - identity or hardware identifiers, and is only ever sent to Macs you - connect to.
Diagnostic log filesMac (/tmp) / device (app container), locallyDebugging. Never uploaded — they leave your device only if you choose - to share them.
- -

Permissions, and why they're needed

- - - - - - - - -
PermissionWhy MeowDisplay asks
Screen Recording (Mac)To capture the virtual display it streams to your device. macOS shows - the purple capture indicator whenever this is active — by design, you - always know.
Accessibility (Mac)To turn taps and scrolls from your device into mouse events on the - Mac.
Local Network (Mac and device)For WiFi discovery and streaming on your own network. Not needed in - USB mode.
-

Every permission is used solely for the function above, on your own machines.

- -

How the connection is protected

-
-

Everything MeowDisplay sends between your Mac and your device — the - video, audio and your input — travels over an encrypted - connection (TLS 1.3), whether over WiFi or a USB cable. If - that encrypted connection can't be set up, MeowDisplay doesn't fall back to - an unencrypted one; it doesn't connect.

-

The first time you connect two devices, both show a short code. You - confirm the codes match, and from then on each device only accepts the - other one it was paired with. Forgetting a device removes that pairing.

-

What this does not cover: encryption protects the connection, not the - devices at either end. Anyone who can use your unlocked Mac or device can - see what's on its screen, and on a shared network others can still see that - two devices are talking, just not what they're sending. The technical - details are in the SECURITY.md file in the - source code.

-
- -

This website

-

This site is a static page hosted on GitHub Pages. It sets no cookies and - runs no analytics or tracking scripts. GitHub may log standard web-server - request data as part of operating the hosting; see the - GitHub - Privacy Statement.

- -

Verifiability

-

MeowDisplay is open source under the - GPL-3.0 license, - the same license as the upstream OpenDisplay project it's built on. - The complete source code of both apps — including everything this page - describes — is public at - github.com/raiseCatError/MeowDisplay. - You don't have to take our word for any of this; you can read the code, build - the apps yourself, or watch the network with the tools of your choice.

- -

Changes and contact

-

If the app's behavior ever changes in a way that affects this page, the page - will be updated in the same repository — its - edit - history is public. Questions or concerns are welcome on the - issue tracker.

- -
-
- - - - - diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index f3de85c71..000000000 --- a/public/robots.txt +++ /dev/null @@ -1,8 +0,0 @@ -# MeowDisplay — free, open-source Sidecar/Duet alternative -# Allow all crawlers full access. -User-agent: * -Allow: / - -# No Sitemap directive yet: MeowDisplay doesn't have its own hosted domain -# (the previous opendisplay.app CNAME was removed). Add sitemap.xml back and -# point this at it once the site is deployed to a MeowDisplay-owned domain. diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 18d901cc1..000000000 --- a/src/App.tsx +++ /dev/null @@ -1,472 +0,0 @@ -import { useEffect, useRef, useState } from "react" -import { AnimatePresence, motion } from "motion/react" -import TextRotate from "./components/TextRotate" - -const HERO_WORDS = [ - "partner's iPad", - "5K iMac", - "spare MacBook", - "iPhone", - "girlfriend's iPad", - "mother's iPad", - "kid's iPad", - "dusty iPad", - "iPad", -] - -export default function App() { - const [starCount, setStarCount] = useState(null) - // Show the brand icon in the navbar only once the big hero logo has - // scrolled up behind the sticky nav — it "hands off" from hero to navbar. - const heroLogoRef = useRef(null) - const [showNavLogo, setShowNavLogo] = useState(false) - - useEffect(() => { - const el = heroLogoRef.current - if (!el || typeof IntersectionObserver === "undefined") return - const io = new IntersectionObserver( - ([entry]) => setShowNavLogo(!entry.isIntersecting), - // 60px = navbar height, so the handoff fires as the logo slips under it. - { rootMargin: "-60px 0px 0px 0px", threshold: 0 } - ) - io.observe(el) - return () => io.disconnect() - }, []) - - // Progressive enhancement: live GitHub star count. - // Fails silent (offline / rate-limited) — the page works without it. - useEffect(() => { - fetch("https://api.github.com/repos/raiseCatError/MeowDisplay", { - headers: { Accept: "application/vnd.github+json" }, - }) - .then((r) => (r.ok ? r.json() : null)) - .then((data) => { - if (data && typeof data.stargazers_count === "number") { - setStarCount(data.stargazers_count.toLocaleString()) - } - }) - .catch(() => {}) - }, []) - - // Scroll-spy: keep the URL fragment in sync with the section at the top of - // the viewport so any section is directly shareable. replaceState avoids - // both a scroll jump and flooding the back button with history entries. - useEffect(() => { - const sections = Array.from(document.querySelectorAll("section")) - if (!sections.length) return - let raf = 0 - const sync = () => { - raf = 0 - // Trigger line just below the 60px sticky nav. - const line = 72 - let current = "" - for (const s of sections) { - if (s.getBoundingClientRect().top <= line) current = s.id - } - const hash = current ? `#${current}` : "" - if (hash !== window.location.hash) { - // Empty hash → drop the fragment entirely (keeps the base path/query). - const url = hash || window.location.pathname + window.location.search - window.history.replaceState(null, "", url) - } - } - const onScroll = () => { - if (!raf) raf = requestAnimationFrame(sync) - } - sync() - window.addEventListener("scroll", onScroll, { passive: true }) - return () => { - window.removeEventListener("scroll", onScroll) - if (raf) cancelAnimationFrame(raf) - } - }, []) - - return ( - <> - - -
-
- MeowDisplay -

Free & open source

-

- - Use your{" "} - - - as your Mac's second monitor. -

-

- A free, open-source alternative to Apple Sidecar, Duet Display and Luna Display. - Use your iPhone and iPad as a second — and even a third — screen for your Mac, - with real touch, mouse and keyboard input routed back, locally over USB/WiFi or - remotely through a private network like Tailscale. No subscription. No dongle. - No MeowDisplay-hosted account or relay. -

-

macOS 14+  ·  iPadOS 16+  ·  iOS 16+  ·  Receiver Mac 12+  ·  GPL-3.0

-

Primary tested configuration: Mac host → iPhone receiver. Other combinations may already work but aren't as thoroughly validated yet.

-
-
- -
-
-

- MeowDisplay is two apps that work together — the Sender on your Mac and a Receiver on the device you want to use as a display. Public prebuilt downloads aren't live yet — build from source today. -

-
-

Sender

-

Install this on the Mac whose desktop you want to extend or mirror.

-
-
-
macOS
-

Captures a virtual display and streams it. Build from source today — a signed, notarized direct download is planned.

- - Build from Source - -

- View on GitHub ↗ -

-
-
-
- -
-

Receiver

-

Install one on the device you want to use as your extra display.

-
-
-
- iOS&iPadOS -
-

The receiver app for your iPhone and iPad. Build and install through Xcode today — a TestFlight beta is planned, with the App Store to follow.

- - Build from Source - -
-
-
macOS
-

- Got a spare Mac to use as the display? The separate MeowDisplay Receiver app - turns an older Mac into a second display — runs on macOS 12+, so older Macs qualify. Build from source today. -

- - Build from Source - -
-
-
- -

- Setup, signing, permissions, and pairing: see the{" "} - Setup Guide ↗. - Want to see what's coming? Check the{" "} - Roadmap ↗. -

-
-
- - {/* No "Demo" section: the only existing demo videos are upstream - OpenDisplay's own recordings (a different developer's product), - so showing them here as MeowDisplay footage would misattribute - them. Re-add once MeowDisplay-branded demo footage exists. */} - -
-
-

Features

-

A true extended display, the way it should be.

-
-
001

No account, ever

No sign-up, no email, no login. And unlike Apple Sidecar — which only works between devices on the same Apple ID — MeowDisplay pairs across different Apple IDs, so you can use a partner's or friend's iPad. Download both apps and go.

-
002

Low-latency pipeline

Up to 60 FPS over USB. Hardware H.264 (VideoToolbox real-time mode), TCP_NODELAY, and frame-dropping backpressure with instant keyframe recovery keep it responsive.

-
003

Two, even three screens

You're not limited to one device. Run several iPads and iPhones at once, each as its own extended display — up to three has been tested, and you can freely mix iPads and iPhones. Arrange them all in System Settings like real monitors.

-
004

Retina sharp

Native Retina resolution — the virtual display matches your device panel pixel-for-pixel at HiDPI (@2x), so text looks exactly like it should.

-
005

USB-wired, lowest latency

Streams over your charging cable via usbmux. No network, no jitter — and your phone charges while it works.

-
006

WiFi, zero config

The phone advertises itself with Bonjour; pick it from a dropdown. No IP addresses to type.

-
007

Touch & scroll

Tap to click, drag to drag, two-finger pan to scroll. A tiny touchscreen for your Mac.

-
008

Portrait mode

Rotate the phone and the virtual display rebuilds as a vertical monitor — perfect for chat, logs, or docs.

-
009

Private by design

One direct TCP connection between your devices. No servers, no accounts, no telemetry. Read the code.

-
010

A spare Mac as a display

Install the small MeowDisplay Receiver app (macOS 12+) on an old Mac and another Mac extends onto it — a real Retina extended display, over WiFi or a Thunderbolt or Ethernet cable. An old MacBook becomes a second monitor.

-
-
-
- -
-
-

Why MeowDisplay

-

The device you already own becomes a real additional display.

-
-
- -

Free, but iPad-only, requires both devices on the same Apple ID, and only on - blessed hardware pairs. iPhones need not apply.

-
-
- -

Pioneered the idea — now behind a subscription.

-
-
- -

Great latency, but you're buying a hardware dongle.

-
-
-
MeowDisplay
-

Free, open source, auditable. The device you already own becomes a real additional - display. If you were about to build your own — contribute here instead.

-
-
- -

How it stacks up

-
- - - - - - - - - - - - - -
MeowDisplayApple SidecarDuetLuna
PriceFree & open sourceFreeSubscription$$$ + dongle
iPhone as display✓✕✓✓
Mac as display✓✕✓✓
Different Apple IDs✓✕✓✓
No account / sign-up✓Apple ID✕—
Wired USB✓✓✓✕
Open source✓—✕✕
-
-
-
- -
-
-

FAQ

-

Questions, answered.

-
-
- How does it actually work? -

The Mac creates a virtual display with the private CGVirtualDisplay API - (the same technique used by BetterDisplay and DeskPad), captures it with ScreenCaptureKit, - hardware-encodes H.264 with VideoToolbox, and streams it over a single TCP connection — - through the USB cable via usbmux, or over WiFi. The phone decodes and renders with - AVSampleBufferDisplayLayer and sends touch coordinates back, which the Mac - injects as mouse events.

-
-
- Is this on the App Store? -

Not yet. The iPhone & iPad receiver is planned for TestFlight and later the App - Store, after broader validation. The Mac app is planned to ship as a signed, notarized - direct download rather than through the Mac App Store, because it relies on{" "} - CGVirtualDisplay, a private API — that's the deal for every virtual-display - product: use it or ship a dongle. You can build either app from source today with your - own (free) Apple developer account in a few minutes — see the{" "} - Setup Guide.

-
-
- Why do I see the purple screen-recording indicator on my Mac? -

macOS shows that privacy indicator for every app that captures the screen — - Duet, Luna, OBS and Zoom included. Apple Sidecar avoids it only because it's built into - the OS. It's a feature, not a bug: you always know a capture is running.

-
-
- WiFi mode can't find my iPhone — why? -

WiFi discovery needs the Local Network permission on both - sides, and macOS/iOS deny it silently if the prompt was missed: check System Settings → - Privacy & Security → Local Network on the Mac, and Settings → Privacy & Security → - Local Network on the iPhone. Both devices must be on the same WiFi and the iPhone app - must be open. The Mac app shows a live permission panel, and the iPhone app has a settings - screen (shake the phone) that links straight there. USB mode needs none of this.

-
-
- iPad support? -

The receiver is a universal iOS app — it runs on iPad today. Run the Mac, an iPhone - and an iPad at once for a second and a third screen. iPad-specific features - (Apple Pencil, pressure) are on the roadmap.

-
-
- Can I use it away from home, not just on the same WiFi? -

Yes — Remote Access lets you connect to a paired Mac through any reachable address, - such as a Tailscale endpoint. Tailscale (or an - equivalent private network) is only used for reachability, not authentication — the - actual trust still comes from the same pinned, encrypted session established during - pairing. There's no MeowDisplay-hosted account or relay involved. One honest limitation: - waking a sleeping Mac remotely over Tailscale isn't solved yet (Wake-on-LAN is a - local-network broadcast) — see the{" "} - roadmap.

-
-
- Can another Mac be the display? -

Yes, where implemented — it needs broader validation than the primary Mac→iPhone setup. - Install MeowDisplay Receiver, a separate small app built from the same source tree, on - the spare Mac. It needs only macOS 12 Monterey or newer (the sending Mac needs 14), so Macs - from around 2015 onward qualify. It appears on the sending Mac like a phone would and - becomes a real extended Retina display. WiFi works out of the box. For a cable, use a - Thunderbolt or USB4 cable (macOS sets up a Thunderbolt Bridge network between - the two), Ethernet, or — on recent macOS on both Macs — a plain USB-C data cable (approve - the "allow accessory" prompt on each Mac). The sender moves the session onto the cable - automatically, even one plugged in mid-session. Keyboard and mouse - input from the receiving Mac is a follow-up.

-
-
- Is any of my screen data sent to a server? -

No. One direct TCP connection between your Mac and your device. No accounts, no - analytics, no cloud. The full story — what the apps store locally, which permissions they - use and why, and the one current caveat about unencrypted WiFi transport — is on the{" "} - privacy page.

-
-
- What's the license? Can I fork it or use it commercially? -

MeowDisplay is licensed under{" "} - GPL-3.0, the - same license as the upstream OpenDisplay project it's built on. You can - use, study, and adapt it freely — including commercially. If you distribute a modified - version, it must remain open source under the same license with the original attribution - intact, so improvements flow back to everyone instead of into closed forks. (Upstream - releases up to v0.4.x were MIT-licensed and remain available under those terms.)

-
-
-
-
- - {/* Community clients for hardware the official apps don't cover. Keep in - sync with the "Compatible apps" section in the README. */} -
-
-

Compatible apps

-

Android, older iPads, Linux.

-

- The official apps are a Mac sender and an iPhone or iPad receiver. Other people have - built their own clients that speak the same protocol, so you can also use an Android - device or an older iPad as a display, or drive one from Linux. If your hardware isn't - covered yet, start here. -

-
- - Android receiver - gprot42/android-opendisplay ↗ -

GrapheneOS receiver for de-Googled Pixel phones and tablets. Android 8.0+.

-
- - Android receiver - josepacelli/opendisplay-android ↗ -

Android receiver that works with an unmodified Mac app. Android 8.0+.

-
- - iOS 12 receiver - cuongpham1/ipad-iphone-second-monitor-ios12-free ↗ -

For iPads and iPhones too old to run the official receiver.

-
- - Linux sender - tixwho/opendisplay-linux ↗ -

Drive an iPhone or iPad from a Wayland desktop (KDE Plasma, Hyprland).

-
-
-

- These are fan-made projects built for the same open protocol, not official builds. They - aren't affiliated with MeowDisplay (or the upstream OpenDisplay project) and aren't - maintained, reviewed or supported by us, so please report issues with them in their own - repositories. Listing them here also says nothing about our own plans: an official - MeowDisplay app may still ship for any of these platforms later. -

-
-
- -
-
-

Contribute

-

Open source, and built in the open.

-

- MeowDisplay is GPL-3.0 and developed entirely on GitHub — the whole stack, from Mac - capture and H.264 encoding to the iOS receiver, is yours to read, build, and improve. - Bug reports, feature ideas, and pull requests are all welcome. Build-and-run instructions - live in the README. -

-
- View on GitHub ↗ - Open an issue ↗ -
-

- New here? Start with the{" "} - README quick-start{" "} - to build both apps, or browse the{" "} - good first issues. -

-
-
- - - - ) -} diff --git a/src/components/TextRotate.tsx b/src/components/TextRotate.tsx deleted file mode 100644 index db6f6a095..000000000 --- a/src/components/TextRotate.tsx +++ /dev/null @@ -1,313 +0,0 @@ -"use client" - -import { - ElementType, - forwardRef, - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useState, -} from "react" -import { - AnimatePresence, - AnimatePresenceProps, - motion, - MotionProps, - Transition, -} from "motion/react" - -import { cn } from "../lib/utils" - -// handy function to split text into characters with support for unicode and emojis -const splitIntoCharacters = (text: string): string[] => { - if (typeof Intl !== "undefined" && "Segmenter" in Intl) { - const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" }) - return Array.from(segmenter.segment(text), ({ segment }) => segment) - } - // Fallback for browsers that don't support Intl.Segmenter - return Array.from(text) -} - -interface TextRotateProps { - texts: string[] - as?: ElementType - rotationInterval?: number - initial?: MotionProps["initial"] | MotionProps["initial"][] - animate?: MotionProps["animate"] | MotionProps["animate"][] - exit?: MotionProps["exit"] | MotionProps["exit"][] - animatePresenceMode?: AnimatePresenceProps["mode"] - animatePresenceInitial?: boolean - staggerDuration?: number - staggerFrom?: "first" | "last" | "center" | number | "random" - transition?: Transition - loop?: boolean - auto?: boolean - splitBy?: "words" | "characters" | "lines" | string - onNext?: (index: number) => void - mainClassName?: string - splitLevelClassName?: string - elementLevelClassName?: string -} - -export interface TextRotateRef { - next: () => void - previous: () => void - jumpTo: (index: number) => void - reset: () => void -} - -interface WordObject { - characters: string[] - needsSpace: boolean -} - -const TextRotate = forwardRef( - ( - { - texts, - as = "p", - transition = { type: "spring", damping: 25, stiffness: 300 }, - initial = { y: "100%", opacity: 0 }, - animate = { y: 0, opacity: 1 }, - exit = { y: "-120%", opacity: 0 }, - animatePresenceMode = "wait", - animatePresenceInitial = false, - rotationInterval = 2000, - staggerDuration = 0, - staggerFrom = "first", - loop = true, - auto = true, - splitBy = "characters", - onNext, - mainClassName, - splitLevelClassName, - elementLevelClassName, - ...props - }, - ref - ) => { - const [currentTextIndex, setCurrentTextIndex] = useState(0) - - // Splitting the text into animation segments - const elements = useMemo(() => { - const currentText = texts[currentTextIndex] - if (splitBy === "characters") { - const text = currentText.split(" ") - return text.map((word, i) => ({ - characters: splitIntoCharacters(word), - needsSpace: i !== text.length - 1, - })) - } - return splitBy === "words" - ? currentText.split(" ") - : splitBy === "lines" - ? currentText.split("\n") - : currentText.split(splitBy) - }, [texts, currentTextIndex, splitBy]) - - // Helper function to calculate stagger delay for each text segment - const getStaggerDelay = useCallback( - (index: number, totalChars: number) => { - const total = totalChars - if (staggerFrom === "first") return index * staggerDuration - if (staggerFrom === "last") return (total - 1 - index) * staggerDuration - if (staggerFrom === "center") { - const center = Math.floor(total / 2) - return Math.abs(center - index) * staggerDuration - } - if (staggerFrom === "random") { - const randomIndex = Math.floor(Math.random() * total) - return Math.abs(randomIndex - index) * staggerDuration - } - return Math.abs(staggerFrom - index) * staggerDuration - }, - [staggerFrom, staggerDuration] - ) - - // Helper function to handle index changes and trigger callback - const handleIndexChange = useCallback( - (newIndex: number) => { - setCurrentTextIndex(newIndex) - onNext?.(newIndex) - }, - [onNext] - ) - - // Go to next text - const next = useCallback(() => { - const nextIndex = - currentTextIndex === texts.length - 1 - ? loop - ? 0 - : currentTextIndex - : currentTextIndex + 1 - - if (nextIndex !== currentTextIndex) { - handleIndexChange(nextIndex) - } - }, [currentTextIndex, texts.length, loop, handleIndexChange]) - - // Go back to previous text - const previous = useCallback(() => { - const prevIndex = - currentTextIndex === 0 - ? loop - ? texts.length - 1 - : currentTextIndex - : currentTextIndex - 1 - - if (prevIndex !== currentTextIndex) { - handleIndexChange(prevIndex) - } - }, [currentTextIndex, texts.length, loop, handleIndexChange]) - - // Jump to specific text by index - const jumpTo = useCallback( - (index: number) => { - const validIndex = Math.max(0, Math.min(index, texts.length - 1)) - if (validIndex !== currentTextIndex) { - handleIndexChange(validIndex) - } - }, - [texts.length, currentTextIndex, handleIndexChange] - ) - - // Reset back to first text - const reset = useCallback(() => { - if (currentTextIndex !== 0) { - handleIndexChange(0) - } - }, [currentTextIndex, handleIndexChange]) - - // Get animation props for each text segment. - const getAnimationProps = useCallback( - (index: number) => { - const getProp = ( - prop: - | MotionProps["initial"] - | MotionProps["initial"][] - | MotionProps["animate"] - | MotionProps["animate"][] - | MotionProps["exit"] - | MotionProps["exit"][] - ) => { - if (Array.isArray(prop)) { - return prop[index % prop.length] - } - return prop - } - - return { - initial: getProp(initial) as MotionProps["initial"], - animate: getProp(animate) as MotionProps["animate"], - exit: getProp(exit) as MotionProps["exit"], - } - }, - [initial, animate, exit] - ) - - // Expose all navigation functions via ref - useImperativeHandle( - ref, - () => ({ - next, - previous, - jumpTo, - reset, - }), - [next, previous, jumpTo, reset] - ) - - // Auto-rotate text - useEffect(() => { - if (!auto) return - const intervalId = setInterval(next, rotationInterval) - return () => clearInterval(intervalId) - }, [next, rotationInterval, auto]) - - // Custom motion component to render the text as a custom HTML tag provided via prop - const MotionComponent = useMemo(() => motion.create(as ?? "p"), [as]) - - return ( - - {texts[currentTextIndex]} - - - - {(splitBy === "characters" - ? (elements as WordObject[]) - : (elements as string[]).map((el, i) => ({ - characters: [el], - needsSpace: i !== elements.length - 1, - })) - ).map((wordObj, wordIndex, array) => { - const previousCharsCount = array - .slice(0, wordIndex) - .reduce((sum, word) => sum + word.characters.length, 0) - - return ( - - {wordObj.characters.map((char, charIndex) => { - const totalIndex = previousCharsCount + charIndex - const animationProps = getAnimationProps(totalIndex) - return ( - - sum + word.characters.length, - 0 - ) - ), - }} - className={"inline-block"} - > - {char} - - - ) - })} - {wordObj.needsSpace && ( - - )} - - ) - })} - - - - ) - } -) - -TextRotate.displayName = "TextRotate" - -export default TextRotate diff --git a/src/entry-server.tsx b/src/entry-server.tsx deleted file mode 100644 index 3734ff837..000000000 --- a/src/entry-server.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { renderToStaticMarkup } from "react-dom/server" -import App from "./App" - -// Used at build time by tools/prerender.mjs to produce the static HTML that gets -// injected into docs/index.html. Effects (the GitHub fetches) don't run here, so -// the prerendered markup reflects the initial, pre-fetch state — same as the old -// static page before its inline script ran. -export function render(): string { - return renderToStaticMarkup() -} diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 881b032a7..000000000 --- a/src/index.css +++ /dev/null @@ -1,275 +0,0 @@ -:root { - --bg: #ffffff; - --fg: #111111; - --muted: #666666; - --faint: #999999; - --line: #e5e5e5; - --line-strong: #111111; - --accent: #0090fc; /* logo blue */ - --maxw: 1040px; - --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace; - --download-control-height: 44px; - --download-control-radius: 7px; - --download-control-border: #a6a6a6; -} -* { box-sizing: border-box; margin: 0; padding: 0; } -html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; } -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - background: var(--bg); color: var(--fg); line-height: 1.55; - font-size: 16px; -webkit-font-smoothing: antialiased; - overflow-x: clip; -} -.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 28px; } -a { color: var(--fg); text-decoration: underline; text-underline-offset: 2px; - text-decoration-thickness: 1px; text-decoration-color: var(--line-strong); } -a:hover { text-decoration-thickness: 2px; } - -/* ---- top nav ---- */ -nav { border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--bg); z-index: 10; } -nav .wrap { display: flex; align-items: center; justify-content: space-between; - height: 60px; } -nav .brand { display: flex; align-items: center; gap: 10px; font-weight: 600; - letter-spacing: -0.01em; text-decoration: none; } -nav .brand img { width: 43px; height: 43px; } -nav .links { display: flex; gap: 22px; font-size: 0.9rem; } -nav .links a { text-decoration: none; color: var(--muted); } -nav .links a:hover { color: var(--fg); } -nav .links a.gh { display: inline-flex; align-items: center; gap: 6px; } -nav .gh-logo { width: 16px; height: 16px; fill: currentColor; display: block; } -nav .gh-stars { font-variant-numeric: tabular-nums; } -/* Seven links plus the star count outgrow the row well before the 680px - cutoff — tighten the spacing instead of dropping an item. */ -@media (max-width: 900px) { nav .links { gap: 14px; font-size: 0.85rem; } } -@media (max-width: 680px) { nav .links { display: none; } } - -/* ---- section scaffolding ---- */ -section { border-top: 1px solid var(--line); } -/* Hero is the first section on the page — no divider above it. */ -section:first-of-type { border-top: 0; } -/* Vertical only — horizontal padding comes from .wrap. Using the padding - shorthand here would reset .wrap's left/right padding to 0 (same - specificity, later rule wins), making content run edge-to-edge on mobile. */ -.sec { padding-top: 84px; padding-bottom: 84px; } -.eyebrow { font-family: var(--mono); font-size: 0.72rem; letter-spacing: 0.14em; - text-transform: uppercase; color: var(--faint); margin-bottom: 20px; } -h1 { font-size: clamp(2.2rem, 6vw, 3.6rem); line-height: 1.04; - letter-spacing: -0.035em; font-weight: 600; } -h2 { font-size: clamp(1.5rem, 3vw, 2rem); line-height: 1.1; letter-spacing: -0.02em; - font-weight: 600; } - -/* ---- hero ---- */ -.hero { padding-top: 48px; padding-bottom: 40px; } /* vertical only — see .sec note */ -/* Downloads live in their own borderless section directly under the hero — - no top divider, so the hero → downloads run reads as one block. */ -.downloads-sec { border-top: 0; padding: 44px 0 84px; } -.downloads-sec .needs-both { margin-top: 0; } -.hero-logo { display: block; width: 160px; height: 160px; margin-bottom: 24px; } -.hero h1 { max-width: 30ch; } -/* two fixed lines: "Use your " then a stable "as your Mac's second monitor." - l1 stays on one line (nowrap) so the rotating word's width never pushes l2 around */ -.hero h1 .l1 { display: block; white-space: nowrap; } -.hero h1 .l2 { display: block; } -/* straight brand-blue underline, thick with rounded tips */ -.hero h1 .u { - text-decoration: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='18' viewBox='0 0 300 18' preserveAspectRatio='none'%3E%3Cpath d='M4,9 L296,9' fill='none' stroke='%230090fc' stroke-width='5' stroke-linecap='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: left 100%; - background-size: 100% 0.4em; - padding-bottom: 0.1em; -} -.hero .tagline { font-size: 1.15rem; color: var(--muted); max-width: 64ch; - margin: 26px 0 0; } -.meta { font-family: var(--mono); font-size: 0.78rem; color: var(--faint); - margin-top: 18px; letter-spacing: 0.02em; } - -/* ---- rotating hero word (fancy TextRotate) ---- */ -/* minimal utility classes the component expects (we don't ship Tailwind) */ -.flex { display: flex; } -.inline-flex { display: inline-flex; } -.inline-block { display: inline-block; } -.flex-wrap { flex-wrap: wrap; } -.flex-col { flex-direction: column; } -.w-full { width: 100%; } -.whitespace-pre-wrap { white-space: pre-wrap; } -.whitespace-pre { white-space: pre; } -.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; - overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } -/* the blue pill — note: must win over .flex's display, so it comes after */ -.rotator-pill { display: inline-flex; vertical-align: baseline; - background: var(--accent); color: #fff; border-radius: 0.16em; - padding: 0 0.18em; margin: 0 0.04em; overflow: hidden; - /* nudge the box so the white text sits optically centred on the line */ - transform: translateY(0.06em); } -/* each split level clips the sliding characters */ -.rotator-line { overflow: hidden; padding-bottom: 0.08em; } - -/* ---- download blocks ---- */ -.needs-both { margin-top: 44px; color: var(--muted); font-size: 0.98rem; } -.needs-both strong { color: var(--fg); } -.download-group { margin-top: 54px; } -.download-role { font-size: clamp(2rem, 4vw, 2.7rem); } -.download-group-intro { color: var(--muted); font-size: 0.98rem; margin-top: 8px; } -.download-options { display: grid; gap: 36px; margin-top: 28px; } -.sender-download { justify-content: center; grid-template-columns: minmax(0, 440px); } -.receiver-downloads { grid-template-columns: repeat(2, minmax(0, 1fr)); } -.download-option { text-align: center; } -@media (max-width: 720px) { - .receiver-downloads { grid-template-columns: 1fr; gap: 48px; } -} -.platform { font-size: clamp(2rem, 4vw, 2.7rem); line-height: 1.1; letter-spacing: -0.02em; - font-weight: 500; margin-bottom: 10px; } -.platform-name { font-weight: 600; } -.platform-separator { color: var(--faint); font-size: 0.55em; font-weight: 400; - margin-inline: 0.24em; vertical-align: 0.12em; } -.platform .ver { font-family: var(--mono); font-size: 0.72rem; letter-spacing: 0.06em; - color: var(--faint); vertical-align: middle; } -.dl-sub { color: var(--muted); font-size: 0.86rem; margin-bottom: 18px; } - -.btn { display: inline-flex; align-items: center; gap: 8px; font: inherit; - font-weight: 600; font-size: 0.95rem; padding: 10px 18px; height: var(--download-control-height); - border-radius: var(--download-control-radius); - text-decoration: none; border: 1px solid var(--download-control-border); cursor: pointer; } -.download-option .btn { display: flex; width: fit-content; margin-inline: auto; } -.download-option .btn { - transition: transform 140ms ease, box-shadow 140ms ease, filter 140ms ease; - box-shadow: 0 1px 1px rgb(0 0 0 / 12%); -} -.download-option .btn:hover { - transform: translateY(-1px); - box-shadow: 0 3px 7px rgb(0 0 0 / 16%); - filter: brightness(1.04); -} -.download-option .btn:active { - transform: translateY(1px) scale(0.98); - box-shadow: 0 1px 1px rgb(0 0 0 / 10%); - transition-duration: 70ms; -} -.download-option .btn:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 3px; -} -@media (prefers-reduced-motion: reduce) { - .download-option .btn { transition: none; } -} -.btn.primary { background: var(--fg); color: var(--bg); } -.btn.primary:hover { background: #000; } -.btn.ghost { background: transparent; color: var(--fg); } -.btn.ghost:hover { background: var(--fg); color: var(--bg); } -.note { font-size: 0.84rem; color: var(--muted); margin-top: 14px; line-height: 1.5; } -.sub { display: block; font-size: 0.84rem; color: var(--muted); margin-top: 12px; } -/* Secondary links under the download buttons share one subtle treatment - (they had drifted — the iOS ones were inheriting the darker default link). */ -.note a, .download-option .sub a { color: var(--muted); text-decoration-color: var(--line); } -.note a:hover, .download-option .sub a:hover { color: var(--fg); text-decoration-color: var(--line-strong); } -.btn-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; } - -/* Demo video: responsive 16:9 embed matching the bordered, squared aesthetic. */ -.video-embed { position: relative; aspect-ratio: 16 / 9; - border: 1px solid var(--line-strong); background: #000; overflow: hidden; } -.video-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; } - -.btn.disabled { opacity: 0.5; cursor: not-allowed; border-style: dashed; } - -/* ---- feature grid ---- */ -.fgrid { display: grid; grid-template-columns: repeat(3, 1fr); - border-top: 1px solid var(--line); border-left: 1px solid var(--line); - margin-top: 36px; } -.fcell { border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); - padding: 24px; } -.fcell .n { font-family: var(--mono); font-size: 0.78rem; color: var(--faint); - display: block; margin-bottom: 12px; } -.fcell h3 { font-size: 1.02rem; font-weight: 600; letter-spacing: -0.01em; margin-bottom: 6px; } -.fcell p { color: var(--muted); font-size: 0.92rem; } -@media (max-width: 860px) { .fgrid { grid-template-columns: repeat(2, 1fr); } } -@media (max-width: 560px) { .fgrid { grid-template-columns: 1fr; } } - -/* ---- "why" comparison rows ---- */ -.compare { border-top: 1px solid var(--line); margin-top: 28px; } -/* 24px horizontal padding = the boxed MeowDisplay row's 2px border + 22px padding, - so every product name and description lines up with the highlighted one */ -.compare .row { display: grid; grid-template-columns: 150px 1fr; gap: 24px; - padding: 22px 24px; border-bottom: 1px solid var(--line); align-items: start; } -.compare .row p { color: var(--muted); max-width: 80ch; } -.compare .row .name { font-weight: 600; color: var(--fg); } -.compare .row.highlight { border: 2px solid var(--accent); - background: #fafafa; padding: 22px; margin-top: 10px; } -@media (max-width: 560px) { .compare .row { grid-template-columns: 1fr; gap: 6px; } } -.tbl-head { margin: 48px 0 16px; font-size: 1.05rem; font-weight: 600; letter-spacing: -0.01em; } - -/* ---- code ---- */ -pre { border: 1px solid var(--line); padding: 20px; - overflow-x: auto; font-size: 0.84rem; line-height: 1.55; background: #fafafa; - margin-top: 8px; } -code { font-family: var(--mono); } -p code { font-size: 0.88em; background: #f2f2f2; padding: 1px 5px; } - -/* ---- comparison table ---- */ -/* no outer frame — keep only the inner row lines (and the boxed MeowDisplay column) */ -.tbl-scroll { overflow-x: auto; } -table { width: 100%; border-collapse: collapse; font-size: 0.92rem; min-width: 660px; } -th, td { padding: 13px 16px; text-align: left; border-bottom: 1px solid var(--line); - vertical-align: top; } -th:first-child, td:first-child { white-space: nowrap; } -tr:last-child td { border-bottom: 0; } -/* product headers: same treatment as the product names in the compare rows above */ -thead th { text-align: center; font-size: 1rem; color: var(--fg); font-weight: 600; } -tbody td:first-child { color: var(--muted); } -.mark-yes { color: var(--fg); font-weight: 600; } -.mark-no { color: var(--faint); } -/* highlight the MeowDisplay column — framed in brand blue */ -th.os, td.os { border-left: 2px solid var(--accent); border-right: 2px solid var(--accent); - background: #fafafa; } -thead th.os { border-top: 2px solid var(--accent); color: var(--fg); } -tbody tr:last-child td.os { border-bottom: 2px solid var(--accent); } - -/* ---- FAQ ---- */ -.faq { border-top: 1px solid var(--line); } -details { border-bottom: 1px solid var(--line); } -summary { cursor: pointer; font-weight: 600; padding: 22px 0; list-style: none; - display: flex; justify-content: space-between; gap: 16px; align-items: baseline; } -summary::-webkit-details-marker { display: none; } -summary::after { content: "+"; font-family: var(--mono); color: var(--faint); - font-weight: 400; font-size: 1.2rem; } -details[open] summary::after { content: "–"; } -details p { color: var(--muted); padding: 0 0 24px; max-width: 78ch; } - -/* ---- compatible apps ---- */ -/* Same hairline-cell grid as .fgrid, but every cell is a link to a community - repo: mono role label on top, repo path as the heading. */ -.compat-lead { color: var(--muted); max-width: 78ch; margin-top: 8px; } -.compat { display: grid; grid-template-columns: repeat(2, 1fr); - border-top: 1px solid var(--line); border-left: 1px solid var(--line); - margin-top: 32px; } -a.compat-item { display: block; padding: 24px; text-decoration: none; color: var(--fg); - border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); } -a.compat-item:hover { background: #fafafa; } -.compat-role { font-family: var(--mono); font-size: 0.78rem; color: var(--faint); - display: block; margin-bottom: 12px; } -/* Repo paths are long (one runs to 45 chars) — allow a mid-path break so a - narrow cell wraps instead of overflowing. */ -.compat-repo { display: block; font-weight: 600; font-size: 1.02rem; - letter-spacing: -0.01em; overflow-wrap: anywhere; } -.compat-item p { color: var(--muted); font-size: 0.92rem; margin-top: 6px; } -.compat-note { color: var(--muted); font-size: 0.9rem; max-width: 78ch; margin-top: 28px; } -@media (max-width: 560px) { - .compat { grid-template-columns: 1fr; } - a.compat-item { padding: 20px; } -} - -/* ---- footer ---- */ -footer { border-top: 1px solid var(--line); padding: 48px 0 64px; - color: var(--muted); font-size: 0.9rem; } -footer .links { display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 14px; } -footer .links a { color: var(--muted); text-decoration-color: var(--line); } -footer .fine { color: var(--faint); font-size: 0.84rem; } - -/* ---- mobile: tighter gutters + less vertical air ---- */ -@media (max-width: 560px) { - .wrap { padding: 0 20px; } - .sec { padding-top: 56px; padding-bottom: 56px; } - .hero { padding-top: 56px; padding-bottom: 60px; } - .hero h1 .l1 { white-space: normal; } /* let the long words wrap on small screens */ - .fcell { padding: 20px; } -} diff --git a/src/lib/utils.ts b/src/lib/utils.ts deleted file mode 100644 index f0e0d18ba..000000000 --- a/src/lib/utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Minimal classnames joiner — stands in for clsx/tailwind-merge. We don't use -// Tailwind, so we only need to flatten and join truthy class strings. -export function cn(...inputs: unknown[]): string { - const out: string[] = [] - const walk = (v: unknown) => { - if (!v) return - if (Array.isArray(v)) { - v.forEach(walk) - return - } - if (typeof v === "string") out.push(v) - } - inputs.forEach(walk) - return out.join(" ") -} diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index 124b1b11f..000000000 --- a/src/main.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { createRoot } from "react-dom/client" -import App from "./App" -import "./index.css" - -createRoot(document.getElementById("root")!).render() diff --git a/tools/prerender.mjs b/tools/prerender.mjs deleted file mode 100644 index 9e77c19b8..000000000 --- a/tools/prerender.mjs +++ /dev/null @@ -1,16 +0,0 @@ -// Post-build static prerender: inject the SSR-rendered markup into the -// built docs/index.html so the shipped page is fully crawlable (SEO parity with -// the old hand-written static site). The client still boots React on top and -// runs the animation; this only fills the initial markup. -import fs from "node:fs/promises" -import { render } from "../dist-ssr/entry-server.js" - -const html = render() -const file = "docs/index.html" -const src = await fs.readFile(file, "utf8") -if (!src.includes('
')) { - throw new Error('prerender: could not find empty
to inject into') -} -const out = src.replace('
', `
${html}
`) -await fs.writeFile(file, out) -console.log(`prerendered ${html.length} chars into ${file}`) diff --git a/tools/press-kit/README.md b/tools/press-kit/README.md index d27e38c63..44f1ca6fa 100644 --- a/tools/press-kit/README.md +++ b/tools/press-kit/README.md @@ -6,4 +6,4 @@ The approved inventory contains final exports only. Raw screenshots, XCTest resu The live website is maintained separately in the private `raiseCatError/MeowDisplayWebsite` repository. Copy the generated `press.html` and `press-kit/` there on a feature branch; run its validate/package scripts. Production is not deployed by this builder. -Artwork sources: `iOS/Assets.xcassets/AppIcon.appiconset/icon-1024.png` and `public/logo.png`, verified against tag `v2.0.0`. See the capture notes inside the kit for screenshot provenance. No original vector or dark/light variant was verified. +Artwork sources: `iOS/Assets.xcassets/AppIcon.appiconset/icon-1024.png` and `press-kit/Brand/MeowDisplay-Logo-512.png`. Provenance: the logo is byte-identical to `public/logo.png` at tag `v2.0.0`, from which it was verified; that file was later removed with the legacy landing page and is not a current source. See the capture notes inside the kit for screenshot provenance. No original vector or dark/light variant was verified. diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 0ce3d4734..000000000 --- a/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noFallthroughCasesInSwitch": true - }, - "include": ["src", "vite.config.ts"] -} diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index 6a114702a..000000000 --- a/vite.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from "vite" -import react from "@vitejs/plugin-react" - -// Served via GitHub Pages (Pages "deploy from branch" → /docs). Relative base -// keeps asset URLs working under whatever subpath (or custom domain) Pages -// serves from, without hardcoding it, and the build emits straight into -// docs/ so the published folder stays the same. -export default defineConfig({ - base: "./", - plugins: [react()], - build: { - outDir: "docs", - emptyOutDir: true, - }, -})